From 2f4e6a6188f6f8714d06f0404abf514a1345e59a Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 13:13:32 +0000 Subject: [PATCH 01/94] Move slugify to helpers --- lib/helpers.js | 51 ++++++++++++++++++++++++++++++-------------- lib/outputmanager.js | 15 ------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 7560974e16..9146c85988 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -74,35 +74,35 @@ function grantSuperPermissions (userId, next) { if (!userId) { return next(new Error("Failed to grant super user permissions, invalid userId: " + userId)); } - + var _assignSuperRole = function (userId, cb) { // verify the Super Admin role is installed rolemanager.retrieveRole({ name: "Super Admin" }, function (err, role) { if (err) { return cb(err); } - + if (role) { return rolemanager.assignRole(role._id, userId, cb); } - + // role does not exist, bah! return cb(new Error('Role not found')); }); }; - + _assignSuperRole(userId, function (err) { if (!err) { // no problem, role was assigned return next(null); } - + // otherwise we need to install default roles rolemanager.installDefaultRoles(function (err) { if (err) { return next(err); } - + // the second attempt passes control back to the first callback _assignSuperRole(userId, next); }); @@ -128,10 +128,10 @@ function isMasterPreviewAccessible(courseId, userId, next) { if (course._isShared || course.createdBy == userId) { // Shared courses on the same tenant are open to users on the same tenant return next(null, true); - } - + } + return next(null, false); - + } else { return next(new Error('Course ' + courseId + ' not found')); } @@ -164,23 +164,23 @@ function hasCoursePermission (action, userId, tenantId, contentItem, next) { if ((course._isShared || course.createdBy == userId) && course._tenantId.toString() == tenantId) { // Shared courses on the same tenant are open to users on the same tenant return next(null, true); - } - + } + return next(null, false); - + } else { return next(new Error('Course ' + courseId + ' not found')); } }); - }); + }); } else { // Course permission cannot be verified return next(null, false); - } + } } /** - * Replaces ALL insstances of the search parameter in a given string with a replacement + * Replaces ALL instances of the search parameter in a given string with a replacement * @param {string} str * @param {string} search * @param {string} replacement @@ -189,10 +189,29 @@ function replaceAll(str, search, replacement) { return str.split(search).join(replacement); } +/** + * Returns a slugified string, e.g. for use in a published filename + * Removes non-word/whitespace chars, converts to lowercase and replaces spaces with hyphens + * Multiple arguments are joined with spaces (and therefore hyphenated) + * @return {string} + **/ +function slugify() { + var str = Array.apply(null,arguments).join(' '); + var strip_re = /[^\w\s-]/g; + var hyphenate_re = /[-\s]+/g; + + str = str.replace(strip_re, '').trim() + str = str.toLowerCase(); + str = str.replace(hyphenate_re, '-'); + + return str; +} + exports = module.exports = { schemaToObject: schemaToObject, grantSuperPermissions: grantSuperPermissions, hasCoursePermission: hasCoursePermission, isMasterPreviewAccessible: isMasterPreviewAccessible, - replaceAll: replaceAll + replaceAll: replaceAll, + slugify: slugify }; diff --git a/lib/outputmanager.js b/lib/outputmanager.js index 0f01936787..6e75c7eb8c 100644 --- a/lib/outputmanager.js +++ b/lib/outputmanager.js @@ -898,21 +898,6 @@ OutputPlugin.prototype.export = function (courseId, req, res, next) { throw new Error('OutputPlugin#export must be implemented by extending objects!'); }; -/** - * Returns a slugified string, e.g. for use in a published filename - * - * @return {string} - */ -OutputPlugin.prototype.slugify = function(s) { - var _slugify_strip_re = /[^\w\s-]/g; - var _slugify_hyphenate_re = /[-\s]+/g; - - s = s.replace(_slugify_strip_re, '').trim().toLowerCase(); - s = s.replace(_slugify_hyphenate_re, '-'); - - return s; -}; - /** * Returns a string with double and single quote characters escaped * From 867c757b5b8cbb3fea941e808906d7a7f1013463 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 13:22:15 +0000 Subject: [PATCH 02/94] Only allow single export zip to be stored per-user at any one time Fixes #1056 --- plugins/output/adapt/index.js | 59 ++++++++++++----------------------- routes/export/index.js | 59 +++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 66 deletions(-) diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js index 341f661627..24a9d34ee7 100644 --- a/plugins/output/adapt/index.js +++ b/plugins/output/adapt/index.js @@ -24,6 +24,7 @@ var origin = require('../../../'), exec = require('child_process').exec, semver = require('semver'), version = require('../../../version'), + helpers = require('../../../lib/helpers'), logger = require('../../../lib/logger'); function AdaptOutput() { @@ -211,7 +212,7 @@ AdaptOutput.prototype.publish = function(courseId, isPreview, request, response, if (!isPreview) { // Now zip the build package var filename = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId, Constants.Filenames.Download); - var zipName = self.slugify(outputJson['course'].title); + var zipName = helpers.slugify(outputJson['course'].title); var output = fs.createWriteStream(filename), archive = archiver('zip'); @@ -257,40 +258,16 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) { var self = this; var tenantId = usermanager.getCurrentUser().tenant._id; var userId = usermanager.getCurrentUser()._id; - var timestamp = new Date().toISOString().replace('T', '-').replace(/:/g, '').substr(0,17); var FRAMEWORK_ROOT_FOLDER = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), Constants.Folders.Framework); var COURSE_ROOT_FOLDER = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId); - - // set in getCourseName - var exportName; - var exportDir; + var exportDir = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.Exports, userId); async.waterfall([ function publishCourse(callback) { self.publish(courseId, true, request, response, callback); }, - function getCourseName(results, callback) { - database.getDatabase(function (error, db) { - if (error) { - return callback(err); - } - - db.retrieve('course', { _id: courseId }, { jsonOnly: true }, function (error, results) { - if (error) { - return callback(error); - } - if(!results || results.length > 1) { - return callback(new Error('Unexpected results returned for course ' + courseId + ' (' + results.length + ')', self)); - } - - exportName = self.slugify(results[0].title) + '-export-' + timestamp; - exportDir = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.Exports, exportName); - callback(); - }); - }); - }, - function copyFiles(callback) { + function copyFiles(results, callback) { self.generateIncludesForCourse(courseId, function(error, includes) { if(error) { return callback(error); @@ -336,20 +313,24 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) { }, function zipFiles(callback) { var archive = archiver('zip'); - var output = fs.createWriteStream(exportDir + '.zip'); - - archive.on('error', callback); - output.on('close', callback); - archive.pipe(output); - archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); - }, - function cleanUp(callback) { - fse.remove(exportDir, function (error) { - callback(error, { zipName: exportName + '.zip' }); - }); + var zipPath = exportDir + '.zip'; + // fse.remove(zipPath, function(error) { + // if(error) return callback(error); + var output = fs.createWriteStream(zipPath); + archive.on('error', callback); + output.on('close', callback); + archive.pipe(output); + archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); + // }); } ], - next); + function onDone(asyncError) { + // remove the exportDir, if there is one + fse.remove(exportDir, function(removeError) { + // async error more important + next(asyncError || removeError); + }); + }); }; /** diff --git a/routes/export/index.js b/routes/export/index.js index ee7c09fb67..36b270f57a 100644 --- a/routes/export/index.js +++ b/routes/export/index.js @@ -41,23 +41,20 @@ server.get('/export/:tenant/:course', function (req, res, next) { plugin.export(course, req, res, function (error, result) { if (error) { logger.log('error', 'Unable to export:', error); - res.statusCode = 500; - return res.json({ + return res.status(500).json({ success: false, message: error.message }); } - - result.success = true; - result.message = app.polyglot.t('app.exportcoursesuccess'); - res.statusCode = 200; - return res.json(result); + return res.status(200).json({ + success: true, + message: app.polyglot.t('app.exportcoursesuccess') + }); }); } }); } else { - res.statusCode = 401; - return res.json({ + return res.status(401).json({ success: false, message: app.polyglot.t('app.errorusernoaccess') }); @@ -65,34 +62,42 @@ server.get('/export/:tenant/:course', function (req, res, next) { }); }); // TODO probably needs to be moved to download route -server.get('/export/:tenant/:course/:title/download.zip', function (req, res, next) { +server.get('/export/:tenant/:course/download.zip', function (req, res, next) { var tenantId = req.params.tenant; var courseId = req.params.course; var userId = usermanager.getCurrentUser()._id; - var zipName = req.params.title; // TODO don't like having to specify this here AND in plugins/output/adapt.export->getCourseName()-exportDir var zipDir = path.join( configuration.tempDir, configuration.getConfig('masterTenantID'), Constants.Folders.Framework, Constants.Folders.Exports, - zipName + userId + '.zip' ); - - fs.stat(zipDir, function(err, stat) { - if (err) { - next(err); - } else { - res.writeHead(200, { - 'Content-Type': 'application/zip', - 'Content-Length': stat.size, - 'Content-disposition' : 'attachment; filename=' + zipName, - 'Pragma' : 'no-cache', - 'Expires' : '0' + // get the course name + app.contentmanager.getContentPlugin('course', function (error, plugin) { + if (error) return callback(error); + plugin.retrieve({ _id:courseId }, {}, function(error, results) { + if (error) { + return callback(error); + } + if (results.length !== 1) { + return callback(new Error('Export: cannot find course (' + courseId + ')')); + } + fs.stat(zipDir, function(error, stat) { + if (error) { + return next(error); + } + var zipName = helpers.slugify(results[0].title,'export') + '.zip'; + res.writeHead(200, { + 'Content-Type': 'application/zip', + 'Content-Length': stat.size, + 'Content-disposition' : 'attachment; filename=' + zipName, + 'Pragma' : 'no-cache', + 'Expires' : '0' + }); + fs.createReadStream(zipDir).pipe(res); }); - - var readStream = fs.createReadStream(zipDir); - readStream.pipe(res); - } + }); }); }); From fad78bb89a4234c16185fe22ca0e96d2a889677b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 16:00:17 +0000 Subject: [PATCH 03/94] Remove unnecessary deletion of old zip The wWrite stream overwrites any existing files with the same name --- plugins/output/adapt/index.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js index 24a9d34ee7..283ca14371 100644 --- a/plugins/output/adapt/index.js +++ b/plugins/output/adapt/index.js @@ -314,14 +314,11 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) { function zipFiles(callback) { var archive = archiver('zip'); var zipPath = exportDir + '.zip'; - // fse.remove(zipPath, function(error) { - // if(error) return callback(error); - var output = fs.createWriteStream(zipPath); - archive.on('error', callback); - output.on('close', callback); - archive.pipe(output); - archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); - // }); + var output = fs.createWriteStream(zipPath); + archive.on('error', callback); + output.on('close', callback); + archive.pipe(output); + archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); } ], function onDone(asyncError) { From d8778b8e8162d88e54d677673bda5e575e560975 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 16:00:32 +0000 Subject: [PATCH 04/94] Fix export download route --- frontend/src/core/editor/global/views/editorView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/core/editor/global/views/editorView.js b/frontend/src/core/editor/global/views/editorView.js index ce7fe98e8c..a59591758b 100644 --- a/frontend/src/core/editor/global/views/editorView.js +++ b/frontend/src/core/editor/global/views/editorView.js @@ -134,7 +134,7 @@ define(function(require){ // get the zip var form = document.createElement("form"); - form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/' + data.zipName + '/download.zip'); + form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/download.zip'); form.submit(); }, error: function(jqXHR, textStatus, errorThrown) { From 6e03617fc0fbee6e16a7dedb885db5f830bce319 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 27 Jun 2016 12:36:46 +0100 Subject: [PATCH 05/94] Set up 'BrowserStorage' plugin to utilise the Web Storage API --- frontend/src/plugins/browserStorage/index.js | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 frontend/src/plugins/browserStorage/index.js diff --git a/frontend/src/plugins/browserStorage/index.js b/frontend/src/plugins/browserStorage/index.js new file mode 100644 index 0000000000..b48112ecd0 --- /dev/null +++ b/frontend/src/plugins/browserStorage/index.js @@ -0,0 +1,58 @@ +// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE +/** + * TODO Look into setting this up like Notify with registered plugins + */ +define(function(require) { + // don't bother doing anything if there's no storage + if(!Storage) return; + + var _ = require('underscore'); + var Origin = require('coreJS/app/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) { + // TODO messy & only handles objects + if(sessionOnly === true) { + if(replaceExisting) userData.session[key] = value; + else userData.session[key] = _.extend({}, userData.session[key], value); + } else { + if(replaceExisting) userData.local[key] = value; + else userData.local[key] = _.extend({}, userData.local[key], value); + } + this.save(); + }, + + get: function(key) { + var value = _.extend({}, userData.local[key], userData.session[key]); + return value; + }, + + // 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)); + } + } + }; + + // init + Origin.on('app:dataReady login:changed', function() { + BrowserStorage.initialise(); + Origin.browserStorage = BrowserStorage; + }); +}); From 69bed18d87c87ae2eff70834c62dd1df19c5bd85 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 27 Jun 2016 12:37:18 +0100 Subject: [PATCH 06/94] Persist dashboard preferences to BrowserStorage --- frontend/src/core/app/views/originView.js | 21 +++++++++---------- .../src/core/dashboard/views/dashboardView.js | 14 ++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/frontend/src/core/app/views/originView.js b/frontend/src/core/app/views/originView.js index 89934e9fce..395201d9c9 100644 --- a/frontend/src/core/app/views/originView.js +++ b/frontend/src/core/app/views/originView.js @@ -51,24 +51,23 @@ define(function(require){ Origin.trigger('router:hideLoading'); }, - setUserPreference: function(key, value) { - if (this.settings.preferencesKey && typeof(Storage) !== "undefined") { - // Get preferences for this view + setUserPreference: function(key, value, sessionOnly) { + if (this.settings.preferencesKey) { var preferences = (Origin.sessionModel.get(this.settings.preferencesKey) || {}); - // Set key and value preferences[key] = value; - // Store in localStorage + // 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/core/dashboard/views/dashboardView.js b/frontend/src/core/dashboard/views/dashboardView.js index 165e1b9c0a..fad7323afb 100644 --- a/frontend/src/core/dashboard/views/dashboardView.js +++ b/frontend/src/core/dashboard/views/dashboardView.js @@ -108,7 +108,7 @@ define(function(require){ title: 1 }; - this.setUserPreference('sort','asc'); + this.setUserPreference('sort','asc', true); if (shouldRenderProjects) { this.updateCollection(true); @@ -123,7 +123,7 @@ define(function(require){ title: -1 } - this.setUserPreference('sort','desc'); + this.setUserPreference('sort','desc', true); if (shouldRenderProjects) { @@ -139,7 +139,7 @@ define(function(require){ updatedAt: -1 } - this.setUserPreference('sort','updated'); + this.setUserPreference('sort','updated', true); if (shouldRenderProjects) { @@ -165,9 +165,9 @@ define(function(require){ if (userPreferences) { var searchString = (userPreferences.search || ''); this.search = this.convertFilterTextToPattern(searchString); - this.setUserPreference('search', searchString); + this.setUserPreference('search', searchString, true); this.tags = (_.pluck(userPreferences.tags, 'id') || []); - this.setUserPreference('tags', userPreferences.tags); + this.setUserPreference('tags', userPreferences.tags, true); } // Check if sort is set and sort the collection @@ -306,12 +306,12 @@ define(function(require){ filterBySearchInput: function (filterText) { this.filterText = filterText; this.search = this.convertFilterTextToPattern(filterText); - this.setUserPreference('search', filterText); + this.setUserPreference('search', filterText, true); this.updateCollection(true); }, filterCoursesByTags: function(tags) { - this.setUserPreference('tags', tags); + this.setUserPreference('tags', tags, true); this.tags = _.pluck(tags, 'id'); this.updateCollection(true); }, From 22a0786be37510b2e0e36ed4a62c2883064d05f4 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 22 Aug 2016 15:43:26 +0100 Subject: [PATCH 07/94] Fix broken ifHasPermissions check Fixes #1344 --- frontend/src/core/app/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/core/app/helpers.js b/frontend/src/core/app/helpers.js index 8943a457e6..35bb0b71a6 100644 --- a/frontend/src/core/app/helpers.js +++ b/frontend/src/core/app/helpers.js @@ -190,7 +190,7 @@ define(function(require){ ifHasPermissions: function(permissions, block) { var permissionsArray = permissions.split(','); - if (Origin.permissions.hasPermissions(permissions)) { + if (Origin.permissions.hasPermissions(permissionsArray)) { return block.fn(this); } else { return block.inverse(this); From 068fb7af44b84c54dbeae560d3b866e6f9078f23 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 22 Aug 2016 15:45:01 +0100 Subject: [PATCH 08/94] Standardise Helpers line breaks --- frontend/src/core/app/helpers.js | 55 +++++++++++++++++++------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/frontend/src/core/app/helpers.js b/frontend/src/core/app/helpers.js index 35bb0b71a6..de7a3a38af 100644 --- a/frontend/src/core/app/helpers.js +++ b/frontend/src/core/app/helpers.js @@ -8,18 +8,23 @@ define(function(require){ 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; @@ -32,6 +37,7 @@ define(function(require){ // Remove _ and spaces with dashes return text.replace(/_| /g, "-").toLowerCase(); }, + keyToTitleString: function(key) { if (!key) { return; @@ -40,29 +46,29 @@ define(function(require){ 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); @@ -78,6 +84,7 @@ define(function(require){ return hh + ':' + mm + ':' + ss; }, + // checks for http/https and www. prefix isAssetExternal: function(url) { if (url && url.length > 0) { @@ -87,6 +94,7 @@ define(function(require){ return true; } }, + ifValueEquals: function(value, text, block) { if (value === text) { return block.fn(this); @@ -94,6 +102,7 @@ define(function(require){ return block.inverse(this); } }, + ifUserIsMe: function(userId, block) { if (userId === Origin.sessionModel.get('id')) { return block.fn(this); @@ -101,6 +110,7 @@ define(function(require){ return block.inverse(this); } }, + selected: function(option, value){ if (option === value) { return ' selected'; @@ -108,27 +118,32 @@ define(function(require){ 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 (window.polyglot != undefined ? window.polyglot.t(str, options) : str); }, + stripHtml: function(html) { return new Handlebars.SafeString(html); }, + bytesToSize: function(bytes) { if (bytes == 0) return '0 B'; @@ -138,6 +153,7 @@ define(function(require){ return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; }, + renderBooleanOptions: function(selectedValue) { var options = ["true", "false"]; var html = ''; @@ -149,6 +165,7 @@ define(function(require){ return new Handlebars.SafeString(html); }, + pickCSV: function (list, key, separator) { var vals = []; separator = (separator && separator.length) ? separator : ','; @@ -163,6 +180,7 @@ define(function(require){ } return vals.join(separator); }, + renderTags: function(list, key) { var html = ''; @@ -173,10 +191,8 @@ define(function(require){ var tag = (key && list[i][key]) ? list[i][key] : list[i]; - html += '
  • ' + tag + '
  • '; } - html += '' } @@ -202,7 +218,7 @@ define(function(require){ 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'); @@ -221,7 +237,6 @@ define(function(require){ }, getThumbnailFromValue: function(url) { - var urlSplit = url.split('/') var fileName = urlSplit[urlSplit.length - 1]; // Get courseAsset model @@ -253,47 +268,45 @@ define(function(require){ }, 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; }, - + validateCourseContent: function(currentCourse) { // Let's do a standard check for at least one child object var containsAtLeastOneChild = true; - var alerts = []; function iterateOverChildren(model) { @@ -314,7 +327,6 @@ define(function(require){ + "' with no " + model._children ); - return; } else { // Go over each child and call validation again @@ -322,7 +334,6 @@ define(function(require){ iterateOverChildren(childModel); }); } - } iterateOverChildren(currentCourse); @@ -348,8 +359,6 @@ define(function(require){ if (isConfirmed) { Origin.trigger('editor:courseValidation'); } - } - }; for(var name in helpers) { From 194c7054d301de23780b82f2ce737a445a27c240 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 22 Aug 2016 15:45:15 +0100 Subject: [PATCH 09/94] Minor formatting changes --- frontend/src/core/app/helpers.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/core/app/helpers.js b/frontend/src/core/app/helpers.js index de7a3a38af..40556ebb40 100644 --- a/frontend/src/core/app/helpers.js +++ b/frontend/src/core/app/helpers.js @@ -193,7 +193,7 @@ define(function(require){ : list[i]; html += '
  • ' + tag + '
  • '; } - html += '' + html += ''; } return html; @@ -214,7 +214,7 @@ define(function(require){ }, getAssetFromValue: function(url) { - var urlSplit = url.split('/') + var urlSplit = url.split('/'); var fileName = urlSplit[urlSplit.length - 1]; // Get courseAsset model var courseAsset = Origin.editor.data.courseAssets.findWhere({_fieldName: fileName}); @@ -222,7 +222,7 @@ define(function(require){ if (courseAsset) { var courseAssetId = courseAsset.get('_assetId'); - return '/api/asset/serve/' + courseAssetId; + return '/api/asset/serve/' + courseAssetId; } else { return ''; } @@ -237,7 +237,7 @@ define(function(require){ }, getThumbnailFromValue: function(url) { - var urlSplit = url.split('/') + var urlSplit = url.split('/'); var fileName = urlSplit[urlSplit.length - 1]; // Get courseAsset model var courseAsset = Origin.editor.data.courseAssets.findWhere({_fieldName: fileName}); @@ -316,7 +316,7 @@ define(function(require){ var currentChildren = model.getChildren(); // Do validate across each item - if (currentChildren.length == 0) { + if (currentChildren.length === 0) { containsAtLeastOneChild = false; alerts.push( @@ -336,6 +336,7 @@ define(function(require){ } } + // call iterator iterateOverChildren(currentCourse); if(alerts.length > 0) { @@ -355,9 +356,10 @@ define(function(require){ return containsAtLeastOneChild; }, - validateCourseConfirm: function(isConfirmed) { - if (isConfirmed) { - Origin.trigger('editor:courseValidation'); + validateCourseConfirm: function(isConfirmed) { + if (isConfirmed) { + Origin.trigger('editor:courseValidation'); + } } }; From 4e4a5670fd225b37db98ca3be066fe136fd8dcc4 Mon Sep 17 00:00:00 2001 From: Tom Greenfield Date: Tue, 29 Aug 2017 13:59:46 +0100 Subject: [PATCH 10/94] Add instruction to component schema --- frontend/src/core/models/componentModel.js | 3 ++- plugins/content/component/model.schema | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/core/models/componentModel.js b/frontend/src/core/models/componentModel.js index 8ab83cedf9..7ea75bbef1 100644 --- a/frontend/src/core/models/componentModel.js +++ b/frontend/src/core/models/componentModel.js @@ -26,7 +26,8 @@ define(function(require) { 'title', 'version', 'themeSettings', - '_onScreen' + '_onScreen', + 'instruction' ] }); diff --git a/plugins/content/component/model.schema b/plugins/content/component/model.schema index 4c37a8d403..114ef684b1 100644 --- a/plugins/content/component/model.schema +++ b/plugins/content/component/model.schema @@ -81,6 +81,12 @@ "validators": [], "translatable": true }, + "instruction": { + "type": "string", + "default" : "", + "inputType": "Text", + "translatable": true + }, "_onScreen": { "type": "object", "title": "", From efa1678505623786fff02bf0c07a3f85d6613df3 Mon Sep 17 00:00:00 2001 From: Ulf Christensen Date: Tue, 3 Oct 2017 13:34:09 +0200 Subject: [PATCH 11/94] Upgrade unzip to unzip2 This fixes #1732 --- package.json | 2 +- plugins/content/bower/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13cf898a36..1ccca5af29 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "semver": "^5.0.3", "serve-favicon": "^2.3.0", "underscore": "~1.5.2", - "unzip": "0.1.8", + "unzip2": "0.2.5", "validator": "4.2.1", "winston": "1.0.2" }, diff --git a/plugins/content/bower/index.js b/plugins/content/bower/index.js index fdc2d273ba..92fe4be8f1 100644 --- a/plugins/content/bower/index.js +++ b/plugins/content/bower/index.js @@ -30,7 +30,7 @@ var origin = require('../../../'), _ = require('underscore'), util = require('util'), path = require('path'), - unzip = require('unzip'), + unzip = require('unzip2'), exec = require('child_process').exec, IncomingForm = require('formidable').IncomingForm, version = require('../../../version.json'); From f0a665d1e4d30ff00e1b646e390fcb207a323b0a Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 30 Oct 2017 12:47:41 +0000 Subject: [PATCH 12/94] Rewrite projectsView.js to fix pagination Fixes #1759 --- .../src/modules/projects/less/qproject.less | 15 - .../modules/projects/views/projectsView.js | 336 +++++++----------- 2 files changed, 119 insertions(+), 232 deletions(-) diff --git a/frontend/src/modules/projects/less/qproject.less b/frontend/src/modules/projects/less/qproject.less index db30d099ab..794b30a448 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 { diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index e6ca357a4c..47f9f75f52 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,272 +7,176 @@ define(function(require){ var ProjectsView = OriginView.extend({ className: 'projects', - settings: { - autoRender: true, - preferencesKey: 'dashboard' + + preRender: function() { + this.settings.preferencesKey = 'dashboard'; + + this.initEventListeners(); + this.initUserPreferences(); }, - preRender: function(options) { - this.setupFilterSettings(); + postRender: function() { + this.initPaging(_.bind(function() { + this.resetCollection(this.setViewToReady); + }, this)); + }, + initEventListeners: function() { 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.initPaging, + 'dashboard:layout:grid': function() { this.doLayout('grid') }, + 'dashboard:layout:list': function() { this.doLayout('list') }, + '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); - this.listenTo(this.collection, { - 'add': this.appendProjectItem, - 'sync': this.checkIfCollectionIsEmpty - }); + $('#app > .app-inner > .contentPane').scroll(_.bind(this.doLazyScroll, 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; + initUserPreferences: function() { + var prefs = this.getUserPreferences(); + this.doLayout(prefs.layout, false); + this.doSort(prefs.sort, false); + this.doFilter(prefs.search, prefs.tags, false); // 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(); - }, - switchLayoutToList: function() { - this.getProjectsContainer().removeClass('grid-layout').addClass('list-layout'); - this.setUserPreference('layout','list'); - }, + prefs = this.getUserPreferences(); - switchLayoutToGrid: function() { - this.getProjectsContainer().removeClass('list-layout').addClass('grid-layout'); - this.setUserPreference('layout','grid'); + 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); - }, + initPaging: function(cb) { + // 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; - sortDescending: function(shouldRenderProjects) { - this.sort = { title: -1 }; - this.setUserPreference('sort','desc'); - if(shouldRenderProjects) this.updateCollection(true); + if(typeof cb === 'function') { + cb(); + } + }, this)); }, - sortLastUpdated: function(shouldRenderProjects) { - this.sort = { updatedAt: -1 }; - this.setUserPreference('sort','updated'); - if (shouldRenderProjects) this.updateCollection(true); + getProjectsContainer: function() { + return this.$('.projects-list'); }, - 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(); - } - // 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); + emptyProjectsContainer: function() { + Origin.trigger('dashboard:dashboardView:removeSubViews'); + this.getProjectsContainer().empty(); }, - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.courseLimit += this.courseDenominator; - this.updateCollection(false); + appendProjectItem: function(model) { + var viewClass = model.isEditable() ? ProjectView : SharedProjectView; + this.getProjectsContainer().append(new viewClass({ model: model }).$el); }, - getProjectsContainer: function() { - return this.$('.projects-list'); + convertFilterTextToPattern: function(filterText) { + var pattern = '.*' + filterText.toLowerCase() + '.*'; + return { title: pattern }; }, - emptyProjectsContainer: function() { - // Trigger event to kill zombie views - Origin.trigger('dashboard:dashboardView:removeSubViews'); - // Empty collection container - this.getProjectsContainer().empty(); + resetCollection: function(cb) { + this.emptyProjectsContainer(); + this.fetchCount = 0; + this.shouldStopFetches = false; + this.collection.reset(); + this.fetchCollection(cb); }, - 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) { + 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(); - }, - - removeTag: function(filterType) { - // remove filter from this.filters - this.tags = _.filter(this.tags, function(item) { return item != filterType; }); - this.filterCollection(); - }, - - filterCollection: function() { - this.search.tags = this.tags.length - ? { $all: this.tags } - : null ; - this.updateCollection(true); - }, - - convertFilterTextToPattern: function(filterText) { - var pattern = '.*' + filterText.toLowerCase() + '.*'; - return { title: pattern}; + doLayout: function(layout) { + var layouts = ["grid", "list"]; + if(_.indexOf(layouts, layout) === -1) { + return; + } + var classSuffix = '-layout'; + this.getProjectsContainer() + .removeClass(layouts.join(classSuffix + ' ') + classSuffix) + .addClass(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(); }, - filterBySearchInput: function (filterText) { - this.filterText = filterText; - this.search = this.convertFilterTextToPattern(filterText); - this.setUserPreference('search', filterText); - this.updateCollection(true); - }, + doFilter: function(text, tags, fetch) { + text = text || ''; + this.filterText = text; + this.search = this.convertFilterTextToPattern(text || ''); + this.setUserPreference('search', text); - filterCoursesByTags: function(tags) { - this.setUserPreference('tags', tags); + tags = tags || []; this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); - }, - - setupLazyScrolling: function() { - var $projectContainer = $('.projects'); - var $projectContainerInner = $('.projects-inner'); - // Remove event before attaching - $projectContainer.off('scroll'); + this.setUserPreference('tags', tags); - $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)); + if(fetch !== false) this.resetCollection(); } }, { template: 'projects' From b770c539f287fbb30a32764d19ec63dcba7c4641 Mon Sep 17 00:00:00 2001 From: Louise McMahon Date: Wed, 1 Nov 2017 11:56:30 +0000 Subject: [PATCH 13/94] article view now generated after article model saved --- .../src/modules/editor/contentObject/views/editorPageView.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index d247d76588..2af692323c 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -127,8 +127,6 @@ define(function(require){ _type:'article' }); - var newArticleView = _this.addArticleView(newPageArticleModel); - newPageArticleModel.save(null, { error: function() { Origin.Notify.alert({ @@ -138,6 +136,7 @@ define(function(require){ }, success: function(model, response, options) { Origin.editor.data.articles.add(model); + var newArticleView = _this.addArticleView(newPageArticleModel); newArticleView.$el.removeClass('syncing').addClass('synced'); newArticleView.addBlock(); } From aef981f9e0f27891a2a59cfb487cb36127742354 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:11:56 +0000 Subject: [PATCH 14/94] Fix issues highlighted by review --- frontend/src/modules/projects/less/projects.less | 4 ++-- frontend/src/modules/projects/less/qproject.less | 5 ----- frontend/src/modules/projects/views/projectsView.js | 11 ++++------- 3 files changed, 6 insertions(+), 14 deletions(-) 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 794b30a448..e474dfc50e 100644 --- a/frontend/src/modules/projects/less/qproject.less +++ b/frontend/src/modules/projects/less/qproject.less @@ -197,8 +197,3 @@ margin-bottom:4px; } } - -.shared-projects-details-label { - color:#9d9d9d; - font-weight: 600; -} diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 47f9f75f52..465bc2741a 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -34,13 +34,13 @@ define(function(require){ }); this.listenTo(this.collection, 'add', this.appendProjectItem); - $('#app > .app-inner > .contentPane').scroll(_.bind(this.doLazyScroll, this)); + $('.contentPane').scroll(_.bind(this.doLazyScroll, this)); }, initUserPreferences: function() { var prefs = this.getUserPreferences(); - this.doLayout(prefs.layout, false); + this.doLayout(prefs.layout); this.doSort(prefs.sort, false); this.doFilter(prefs.search, prefs.tags, false); // set relevant filters as selected @@ -142,10 +142,7 @@ define(function(require){ if(_.indexOf(layouts, layout) === -1) { return; } - var classSuffix = '-layout'; - this.getProjectsContainer() - .removeClass(layouts.join(classSuffix + ' ') + classSuffix) - .addClass(layout + '-layout'); + this.getProjectsContainer().attr('data-layout', layout); this.setUserPreference('layout', layout); }, @@ -169,7 +166,7 @@ define(function(require){ doFilter: function(text, tags, fetch) { text = text || ''; this.filterText = text; - this.search = this.convertFilterTextToPattern(text || ''); + this.search = this.convertFilterTextToPattern(text); this.setUserPreference('search', text); tags = tags || []; From 1d334feb07f7937c1df93650dab68ea16f79b948 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:12:51 +0000 Subject: [PATCH 15/94] Make dashboard layouts a bit less hard-coded --- .../src/modules/projects/views/projectsView.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 465bc2741a..30c3bfb6ab 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -7,6 +7,10 @@ define(function(require){ var ProjectsView = OriginView.extend({ className: 'projects', + supportedLayouts: [ + "grid", + "list" + ], preRender: function() { this.settings.preferencesKey = 'dashboard'; @@ -23,15 +27,18 @@ define(function(require){ initEventListeners: function() { this.listenTo(Origin, { - 'window:resize': this.initPaging, - 'dashboard:layout:grid': function() { this.doLayout('grid') }, - 'dashboard:layout:list': function() { this.doLayout('list') }, + '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.supportedLayouts.forEach(function(layout) { + this.listenTo(Origin, 'dashboard:layout:' + layout, function() { this.doLayout(layout); }); + }, this); + this.listenTo(this.collection, 'add', this.appendProjectItem); $('.contentPane').scroll(_.bind(this.doLazyScroll, this)); @@ -138,8 +145,7 @@ define(function(require){ }, doLayout: function(layout) { - var layouts = ["grid", "list"]; - if(_.indexOf(layouts, layout) === -1) { + if(this.supportedLayouts.indexOf(layout) === -1) { return; } this.getProjectsContainer().attr('data-layout', layout); From e1f002ba2b50c33a196fd733318405bfd02863ec Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:13:28 +0000 Subject: [PATCH 16/94] Fix issues with window resizing and dashboard paging --- .../modules/projects/views/projectsView.js | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 30c3bfb6ab..962f2da179 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -12,17 +12,11 @@ define(function(require){ "list" ], - preRender: function() { + postRender: function() { this.settings.preferencesKey = 'dashboard'; - - this.initEventListeners(); this.initUserPreferences(); - }, - - postRender: function() { - this.initPaging(_.bind(function() { - this.resetCollection(this.setViewToReady); - }, this)); + this.initEventListeners(); + this.initPaging(); }, initEventListeners: function() { @@ -60,7 +54,21 @@ define(function(require){ Origin.trigger('sidebar:update:ui', prefs); }, - initPaging: function(cb) { + // Set some default preferences + getUserPreferences: function() { + var prefs = OriginView.prototype.getUserPreferences.apply(this, arguments); + + if(!prefs.layout) prefs.layout = 'grid'; + if(!prefs.sort) prefs.sort = 'asc'; + + return prefs; + }, + + initPaging: function() { + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = -1; + } // we need to load one course first to check page size this.pageSize = 1; this.resetCollection(_.bind(function(collection) { @@ -73,10 +81,8 @@ define(function(require){ // 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; - - if(typeof cb === 'function') { - cb(); - } + // need another reset to get the actual pageSize number of items + this.resetCollection(this.setViewToReady); }, this)); }, @@ -180,6 +186,14 @@ define(function(require){ this.setUserPreference('tags', tags); if(fetch !== false) this.resetCollection(); + }, + + onResize: function() { + // we don't want to re-initialise for _every_ resize event + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + } + this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); } }, { template: 'projects' From bc47e8c224250fb2c9b93adb8b0bc14eac1c1007 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:13:47 +0000 Subject: [PATCH 17/94] Fix bug with restoring dashboard preferences --- frontend/src/modules/projects/views/projectsView.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 962f2da179..84e41f5456 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -45,11 +45,10 @@ define(function(require){ this.doSort(prefs.sort, false); this.doFilter(prefs.search, prefs.tags, false); // set relevant filters as selected - $("a[data-callback='dashboard:layout:grid']").addClass('selected'); - $("a[data-callback='dashboard:sort:asc']").addClass('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); }, From ce95d140f7788789628396355defa0b75c7eed14 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 23 Nov 2017 09:36:04 +0000 Subject: [PATCH 18/94] Fix spacing size --- .../views/assetManagementCollectionView.js | 382 +++++++++--------- 1 file changed, 181 insertions(+), 201 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index ce196eb7fe..bfac739fee 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -1,205 +1,185 @@ // 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); - }, - - 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); + 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(); + } } - - }, { - template: 'assetManagementCollection' - }); - - return AssetCollectionView; - + }, 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); + }, + + 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); + } + }, { + template: 'assetManagementCollection' + }); + return AssetCollectionView; }); From 77a461e96d3bcec39ebb30256f7b5c51642c99a4 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 23 Nov 2017 10:18:08 +0000 Subject: [PATCH 19/94] Shift code around to group similar behaviour --- .../views/assetManagementCollectionView.js | 181 +++++++++--------- 1 file changed, 89 insertions(+), 92 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index bfac739fee..e31f36f9e9 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -1,52 +1,56 @@ // 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); + sort: { + createdAt: -1 }, + search: {}, + filters: [], + tags: [], + assetLimit: -32, + assetDenominator: 32, + collectionLength: 0, + shouldStopFetches: false, - 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); + preRender: function(options) { + if(options.search) { + this.search = options.search; + var assetType = this.search.assetType; + if(assetType) this.filters = assetType.$in; } + this.initEventListeners(); + }, + + 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(); + }, + + 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.updateCollection + }); + this.listenTo(this.collection, { + 'add': this.appendAssetItem, + 'sync': this.onCollectionSynced + }); + }, + + appendAssetItem: function (asset) { + this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); }, setupLazyScrolling: function() { @@ -55,46 +59,34 @@ define(function(require){ // Remove event before attaching $assetContainer.off('scroll'); $assetContainer.on('scroll', _.bind(function() { + var scrollableHeight = $assetContainerInner.height() - $assetContainer.height(); 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(); - } + var isAtBottom = (scrollableHeight - scrollTop) < 30; + if (isAtBottom && !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'); + removeLazyScrolling: function() { + $('.asset-management-assets-container').off('scroll'); + }, - // Reset fetches cache - this.shouldStopFetches = false; + /** + * Collection manipulation + */ - this.assetLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } + updateCollection: function (reset) { + if (reset) this.resetCollection(); if (!Origin.permissions.hasPermissions(["*"])) { - this.search = _.extend(this.search, {_isDeleted: false}); + this.search = _.extend(this.search, { _isDeleted: false }); } this.search = _.extend(this.search, { - tags: { - $all: this.tags - } - }, { - assetType: { - $in: this.filters - } + 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 @@ -113,21 +105,36 @@ define(function(require){ } }, 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.shouldStopFetches = this.collectionLength === this.collection.length; + 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); + onCollectionSynced: function () { + $('.asset-management-no-assets').toggleClass('display-none', this.collection.length === 0); + // 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) { + return; + } + $('.asset-management-assets-container').hide(); + _.delay(function() { + $('.asset-management-assets-container').show(); + }, 10); + }, + + resetCollection: function() { + // Trigger event to kill zombie views + Origin.trigger('assetManagement:assetViews:remove'); + + this.shouldStopFetches = false; + this.assetLimit = 0; + this.collectionLength = 0; + this.collection.reset(); }, lazyRenderCollection: function() { @@ -136,32 +143,26 @@ define(function(require){ 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(); + /** + * Filtering + */ + + filterCollection: function() { + this.search.assetType = this.filters.length ? { $in: this.filters } : null; + this.updateCollection(true); }, 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.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); - }, - filterBySearchInput: function (filterText) { var pattern = '.*' + filterText.toLowerCase() + '.*'; this.search = { title: pattern, description: pattern }; @@ -170,16 +171,12 @@ define(function(require){ $(".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); } }, { - template: 'assetManagementCollection' + template: 'assetManagementCollection' }); return AssetCollectionView; }); From 9e932b025d6617483137081aa4491dc2e11989fe Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 14:07:40 +0000 Subject: [PATCH 20/94] Fix indentation --- frontend/src/modules/assetManagement/index.js | 49 ++++++++--------- .../views/assetManagementView.js | 53 ++++++++----------- 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index d9ddcc5a5a..a493bb24ca 100644 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -14,35 +14,32 @@ define(function(require) { Origin.assetManagement.filterData = {}; if (!location) { - var tagsCollection = new TagsCollection(); - - tagsCollection.fetch({ - success: function() { - // 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: tagsCollection}).$el); - Origin.contentPane.setView(AssetManagementView, {collection: assetCollection}); - Origin.trigger('assetManagement:loaded'); - }, - error: function() { - console.log('Error occured getting the tags collection - try refreshing your page'); - } - }); - } else if (location=== 'new') { - Origin.trigger('location:title:update', {title: 'New Asset'}); - Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); + (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: tagsCollection }).$el); + Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); + Origin.trigger('assetManagement:loaded'); + }, + error: function() { + console.log('Error occured getting the tags collection - try refreshing your page'); + } + }); + } else if (location === 'new') { + Origin.trigger('location:title:update', { title: 'New Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); } else if (subLocation === 'edit') { - var Asset = new AssetModel({ _id: location }); // Fetch existing asset model - Asset.fetch({ - success: function() { - Origin.trigger('location:title:update', {title: 'Edit Asset'}); + (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: Asset }); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: model }); } }); } diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index dbc757171c..81f5c18059 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -1,34 +1,27 @@ // 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.resizeAssetPanels, + 'assetManagement:assetItemView:preview': this.onAssetClicked, + 'assetManagement:assetPreviewView:delete': this.onAssetDeleted + }); }, postRender: function() { - this.setupSubViews(); - this.resizeAssetPanels(); - // Set imageReady - _.defer(_.bind(this.setupImageReady, this)); + this.setupSubViews(); + this.resizeAssetPanels(); + // Set imageReady + _.defer(_.bind(this.setupImageReady, this)); }, setupImageReady: function() { @@ -36,34 +29,30 @@ define(function(require){ }, setupSubViews: function() { - // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection}).$el); + // 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); + 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); }, 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(); + this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ model: model }).$el); }, onAssetDeleted: function() { - this.$('.asset-management-no-preview').show(); + this.$('.asset-management-no-preview').show(); } - }, { template: 'assetManagement' }); return AssetManagementView; - }); From 037ade536b9cf764b22bd918279ffab974fc424f Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:00:10 +0000 Subject: [PATCH 21/94] Refactor into separate functions --- frontend/src/modules/assetManagement/index.js | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index a493bb24ca..db159f08bf 100644 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -10,39 +10,12 @@ define(function(require) { var TagsCollection = require('core/collections/tagsCollection'); Origin.on('router:assetManagement', function(location, subLocation, action) { - Origin.assetManagement = {}; - Origin.assetManagement.filterData = {}; - - if (!location) { - (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: tagsCollection }).$el); - Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); - Origin.trigger('assetManagement:loaded'); - }, - error: function() { - console.log('Error occured getting the tags collection - try refreshing your page'); - } - }); - } else if (location === 'new') { - Origin.trigger('location:title:update', { title: 'New Asset' }); - Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); - } else if (subLocation === 'edit') { - // 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 }); - } - }); - } + Origin.assetManagement = { + filterData: {} + }; + if(!location) return loadAssetsView(); + if(location === 'new') loadNewAssetView(); + if(subLocation === 'edit') loadEditAssetView(); }); Origin.on('globalMenu:assetManagement:open', function() { @@ -58,4 +31,39 @@ define(function(require) { "sortOrder": 2 }); }); + + 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: tagsCollection }).$el); + Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); + Origin.trigger('assetManagement:loaded'); + }, + error: function() { + console.log('Error occured getting the tags collection - try refreshing your page'); + } + }); + } + + function loadNewAssetView() { + Origin.trigger('location:title:update', { title: 'New Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); + } + + function loadEditAssetView() { + // 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 }); + } + }); + } }); From 37b16bcf85bd5d08b0f8d01bdfa133edde3623fb Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:00:53 +0000 Subject: [PATCH 22/94] Add asset._isDeleted check to back-end --- lib/assetmanager.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/assetmanager.js b/lib/assetmanager.js index 6ef5b1e592..58da74d21d 100644 --- a/lib/assetmanager.js +++ b/lib/assetmanager.js @@ -183,13 +183,12 @@ exports = module.exports = { */ retrieveAsset: function (search, options, next) { - + var __this = this; // shuffle params if ('function' === typeof options) { next = options; options = {}; } - // Ensure the tags are populated var pop = { tags: '_id title' }; if (!options.populate) { @@ -197,18 +196,20 @@ exports = module.exports = { } else { options.populate = _.extend(pop, options.populate); } - database.getDatabase(function (error, db) { if (error) { return next(error); } - - db.retrieve('asset', search, options, function (error, records) { + var user = usermanager.getCurrentUser(); + // only return deleted assets if user has correct permissions + __this.hasPermission('delete', user._id, user.tenant._id, '*', function(error, isAllowed) { if (error) { return next(error); } - - return next(null, records); + if(!isAllowed) { + search = _.extend(search, { _isDeleted: false }); + } + db.retrieve('asset', search, options, next); }); }); }, From 375fd2e61ebec36adaf3153c778c2baa9ec1a85b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:02:42 +0000 Subject: [PATCH 23/94] Refactor for readability --- .../assetManagement/views/assetManagementView.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index 81f5c18059..61ca811ee8 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -18,7 +18,9 @@ define(function(require){ }, postRender: function() { - this.setupSubViews(); + var view = new AssetManagementCollectionView({ collection: this.collection }); + this.$('.asset-management-assets-container-inner').append(view.$el); + this.resizeAssetPanels(); // Set imageReady _.defer(_.bind(this.setupImageReady, this)); @@ -28,11 +30,6 @@ define(function(require){ 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(); @@ -44,7 +41,9 @@ define(function(require){ onAssetClicked: function(model) { this.$('.asset-management-no-preview').hide(); - this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ model: model }).$el); + + var view = new AssetManagementPreviewView({ model: model }); + this.$('.asset-management-preview-container-inner').html(view.$el); }, onAssetDeleted: function() { From 3e724bebbf378138737320a7f86ca899b155ba06 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:33:05 +0000 Subject: [PATCH 24/94] Refactor for brevity --- .../assetManagement/views/assetManagementView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index 61ca811ee8..992de99696 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -22,12 +22,10 @@ define(function(require){ this.$('.asset-management-assets-container-inner').append(view.$el); this.resizeAssetPanels(); - // Set imageReady - _.defer(_.bind(this.setupImageReady, this)); - }, - - setupImageReady: function() { - this.$el.imageready(this.setViewToReady); + // defer setting ready status until images are ready + _.defer(function() { + view.$el.imageready(this.setViewToReady); + }); }, resizeAssetPanels: function() { @@ -41,7 +39,7 @@ define(function(require){ onAssetClicked: function(model) { this.$('.asset-management-no-preview').hide(); - + var view = new AssetManagementPreviewView({ model: model }); this.$('.asset-management-preview-container-inner').html(view.$el); }, From a4a60bab38ff310ae779a4f5954055d32626aed2 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 16:06:05 +0000 Subject: [PATCH 25/94] Fix scrollbar position --- .../assetManagement/views/assetManagementView.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index 992de99696..fcc1d462b4 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -11,7 +11,7 @@ define(function(require){ preRender: function() { this.listenTo(Origin, { - 'window:resize': this.resizeAssetPanels, + 'window:resize': this.resizePanels, 'assetManagement:assetItemView:preview': this.onAssetClicked, 'assetManagement:assetPreviewView:delete': this.onAssetDeleted }); @@ -21,20 +21,25 @@ define(function(require){ var view = new AssetManagementCollectionView({ collection: this.collection }); this.$('.asset-management-assets-container-inner').append(view.$el); - this.resizeAssetPanels(); + this.resizePanels(); // defer setting ready status until images are ready _.defer(function() { view.$el.imageready(this.setViewToReady); }); }, - resizeAssetPanels: function() { + resizePanels: 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); + 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) { From 206747832ea0ac9dd026b3b831166df0e96f2fb0 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 16:06:43 +0000 Subject: [PATCH 26/94] Fix asset management paging Fixes #1790 --- .../modules/assetManagement/less/asset.less | 3 +- .../views/assetManagementCollectionView.js | 163 +++++++++--------- 2 files changed, 81 insertions(+), 85 deletions(-) diff --git a/frontend/src/modules/assetManagement/less/asset.less b/frontend/src/modules/assetManagement/less/asset.less index a421973117..21da7efc46 100644 --- 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 e31f36f9e9..3d82b5ddc6 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -8,16 +8,13 @@ define(function(require){ tagName: "div", className: "asset-management-collection", - sort: { - createdAt: -1 - }, + sort: { createdAt: -1 }, search: {}, filters: [], tags: [], - assetLimit: -32, - assetDenominator: 32, - collectionLength: 0, + fetchCount: 0, shouldStopFetches: false, + pageSize: 1, preRender: function(options) { if(options.search) { @@ -29,10 +26,9 @@ define(function(require){ }, 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(); + this.initPaging(); + // init lazy scrolling + $('.asset-management-assets-container').scroll(_.bind(this.doLazyScroll, this)); }, initEventListeners: function() { @@ -41,106 +37,83 @@ define(function(require){ 'assetManagement:sidebarFilter:remove': this.removeFilter, 'assetManagement:sidebarView:filter': this.filterBySearchInput, 'assetManagement:assetManagementSidebarView:filterByTags': this.filterByTags, - 'assetManagement:collection:refresh': this.updateCollection - }); - this.listenTo(this.collection, { - 'add': this.appendAssetItem, - 'sync': this.onCollectionSynced + 'assetManagement:collection:refresh': this.fetchCollection }); + this.listenTo(this.collection, 'add', this.appendAssetItem); }, - appendAssetItem: function (asset) { - this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); - }, - - 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 scrollableHeight = $assetContainerInner.height() - $assetContainer.height(); - var scrollTop = $assetContainer.scrollTop(); - var isAtBottom = (scrollableHeight - scrollTop) < 30; - if (isAtBottom && !this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } + initPaging: function() { + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = -1; + } + this.resetCollection(_.bind(function(collection) { + var containerHeight = $(window).height()-this.$el.offset().top; + var containerWidth = this.$el.width(); + 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)); }, - removeLazyScrolling: function() { - $('.asset-management-assets-container').off('scroll'); + appendAssetItem: function (asset) { + this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); }, /** * Collection manipulation */ - updateCollection: function (reset) { - if (reset) this.resetCollection(); - - 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) { + 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 }, + assetType: { $in: this.filters } + }), operators : { - skip: this.assetLimit, - limit: this.assetDenominator, + skip: this.fetchCount, + limit: this.pageSize, sort: this.sort } }, - success: _.bind(function() { - this.shouldStopFetches = this.collectionLength === this.collection.length; - 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; + + $('.asset-management-no-assets').toggleClass('display-none', this.fetchCount > 0); + Origin.trigger('assetManagement:assetManagementCollection:fetched'); - }, this) + if(typeof cb === 'function') cb(collection); + }, this), + error: function(error) { + console.log(error); + this.isCollectionFetching = false; + } }); }, - onCollectionSynced: function () { - $('.asset-management-no-assets').toggleClass('display-none', this.collection.length === 0); - // 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) { - return; - } - $('.asset-management-assets-container').hide(); - _.delay(function() { - $('.asset-management-assets-container').show(); - }, 10); - }, - - resetCollection: function() { - // Trigger event to kill zombie views + resetCollection: function(cb) { + // to remove old views Origin.trigger('assetManagement:assetViews:remove'); this.shouldStopFetches = false; - this.assetLimit = 0; - this.collectionLength = 0; + this.fetchCount = 0; this.collection.reset(); - }, - - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.assetLimit += this.assetDenominator; - this.updateCollection(false); + this.fetchCollection(cb); }, /** @@ -149,7 +122,7 @@ define(function(require){ filterCollection: function() { this.search.assetType = this.filters.length ? { $in: this.filters } : null; - this.updateCollection(true); + this.fetchCollection(); }, addFilter: function(filterType) { @@ -166,14 +139,38 @@ define(function(require){ filterBySearchInput: function (filterText) { var pattern = '.*' + filterText.toLowerCase() + '.*'; this.search = { title: pattern, description: pattern }; - this.updateCollection(true); + this.fetchCollection(); $(".asset-management-modal-filter-search" ).focus(); }, filterByTags: function(tags) { this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); + this.fetchCollection(); + }, + + /** + * Event handling + */ + + onResize: function() { + // we don't want to re-initialise for _every_ resize event + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + } + this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); + }, + + 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(); } }, { template: 'assetManagementCollection' From 6af5ac25e833a6efe94a6928991051d8bcafeb30 Mon Sep 17 00:00:00 2001 From: thomas Date: Tue, 5 Dec 2017 13:20:28 +0100 Subject: [PATCH 27/94] store scroll position in origin.editor --- .../contentObject/views/editorPageView.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index 2af692323c..01e9cb8688 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -25,13 +25,14 @@ define(function(require){ preRender: function() { this.setupChildCount(); - this.listenTo(Origin, { '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); + + this._onScroll = _.bind(_.throttle(this.onScroll, 400), this); }, resize: function() { @@ -66,10 +67,14 @@ define(function(require){ evaluateChildStatus: function() { this.childrenRenderedCount++; + + if (this.childrenRenderedCount < this.childrenCount) return; + this.allChildrenRendered(); }, postRender: function() { this.addArticleViews(); + this.setupScrollListener(); _.defer(_.bind(function(){ this.resize(); @@ -168,7 +173,31 @@ define(function(require){ 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() { + $('.contentPane').scrollTop(Origin.editor.scrollTo); + }, + + remove: function() { + this.removeScrollListener(); + + EditorOriginView.prototype.remove.apply(this, arguments); } + }, { template: 'editorPage' }); From 0c2a92a2c2a23e2ff7aa17a0e3d5061dc46ee65f Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 6 Dec 2017 15:08:26 +0100 Subject: [PATCH 28/94] throttle callbacks use on to properly remove scroll listener in remove function --- .../modules/projects/views/projectsView.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 84e41f5456..1dca27ded4 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -20,8 +20,11 @@ define(function(require){ }, initEventListeners: function() { + this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); + this._onResize = _.bind(_.throttle(this.onResize, 250), this); + this.listenTo(Origin, { - 'window:resize': this.onResize, + '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'); }, @@ -35,7 +38,7 @@ define(function(require){ this.listenTo(this.collection, 'add', this.appendProjectItem); - $('.contentPane').scroll(_.bind(this.doLazyScroll, this)); + $('.contentPane').on('scroll', this._doLazyScroll); }, initUserPreferences: function() { @@ -188,12 +191,15 @@ define(function(require){ }, onResize: function() { - // we don't want to re-initialise for _every_ resize event - if(this.resizeTimer) { - clearTimeout(this.resizeTimer); - } - this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); + this.initPaging(); + }, + + remove: function() { + $('.contentPane').off('scroll', this._doLazyScroll); + + OriginView.prototype.remove.apply(this, arguments); } + }, { template: 'projects' }); From 8c2f25d361b0e5f757550d01a34a9e823ae9ab23 Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 6 Dec 2017 16:03:10 +0100 Subject: [PATCH 29/94] use debounce instead of throttle --- frontend/src/modules/projects/views/projectsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 1dca27ded4..a9d0bd622e 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -21,7 +21,7 @@ define(function(require){ initEventListeners: function() { this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); - this._onResize = _.bind(_.throttle(this.onResize, 250), this); + this._onResize = _.bind(_.debounce(this.onResize, 250), this); this.listenTo(Origin, { 'window:resize': this._onResize, From 6ea27c893b623d7eeb25fe04483870cfb9cc6fdb Mon Sep 17 00:00:00 2001 From: lc-thomasberger Date: Thu, 7 Dec 2017 14:03:12 +0100 Subject: [PATCH 30/94] Fix asset filters (#1810) * update object syntax * pass in location into funciton, otherwise window.location is used This fixes asset edit mode * remove scroll handler * fixed filter * throttle scroll, debounce resize --- frontend/src/modules/assetManagement/index.js | 4 +- .../views/assetManagementCollectionView.js | 40 ++++++++++++------- .../views/assetManagementNewAssetView.js | 2 +- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index db159f08bf..9f6171331c 100644 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -15,7 +15,7 @@ define(function(require) { }; if(!location) return loadAssetsView(); if(location === 'new') loadNewAssetView(); - if(subLocation === 'edit') loadEditAssetView(); + if(subLocation === 'edit') loadEditAssetView(location); }); Origin.on('globalMenu:assetManagement:open', function() { @@ -56,7 +56,7 @@ define(function(require) { Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); } - function loadEditAssetView() { + function loadEditAssetView(location) { // Fetch existing asset model (new AssetModel({ _id: location })).fetch({ success: function(model) { diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index 3d82b5ddc6..a0a15ca5e1 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -5,7 +5,6 @@ define(function(require){ var AssetItemView = require('./assetManagementItemView'); var AssetCollectionView = OriginView.extend({ - tagName: "div", className: "asset-management-collection", sort: { createdAt: -1 }, @@ -23,12 +22,16 @@ define(function(require){ 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').scroll(_.bind(this.doLazyScroll, this)); + $('.asset-management-assets-container').on('scroll', this._doLazyScroll); + $(window).on('resize', this._onResize); }, initEventListeners: function() { @@ -43,12 +46,8 @@ define(function(require){ }, initPaging: function() { - if(this.resizeTimer) { - clearTimeout(this.resizeTimer); - this.resizeTimer = -1; - } this.resetCollection(_.bind(function(collection) { - var containerHeight = $(window).height()-this.$el.offset().top; + var containerHeight = $(window).height() - this.$el.offset().top; var containerWidth = this.$el.width(); var itemHeight = $('.asset-management-list-item').outerHeight(true); var itemWidth = $('.asset-management-list-item').outerWidth(true); @@ -57,6 +56,7 @@ define(function(require){ // 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)); @@ -71,7 +71,7 @@ define(function(require){ */ fetchCollection: function(cb) { - if(this.shouldStopFetches) { + if(this.shouldStopFetches || this.isCollectionFetching) { return; } this.isCollectionFetching = true; @@ -106,14 +106,17 @@ define(function(require){ }); }, - resetCollection: function(cb) { + resetCollection: function(cb, shouldFetch) { // to remove old views Origin.trigger('assetManagement:assetViews:remove'); this.shouldStopFetches = false; this.fetchCount = 0; this.collection.reset(); - this.fetchCollection(cb); + + if (shouldFetch === undefined || shouldFetch === true) { + this.fetchCollection(cb); + } }, /** @@ -121,6 +124,7 @@ define(function(require){ */ filterCollection: function() { + this.resetCollection(null, false); this.search.assetType = this.filters.length ? { $in: this.filters } : null; this.fetchCollection(); }, @@ -137,6 +141,7 @@ define(function(require){ }, filterBySearchInput: function (filterText) { + this.resetCollection(null, false); var pattern = '.*' + filterText.toLowerCase() + '.*'; this.search = { title: pattern, description: pattern }; this.fetchCollection(); @@ -145,6 +150,7 @@ define(function(require){ }, filterByTags: function(tags) { + this.resetCollection(null, false); this.tags = _.pluck(tags, 'id'); this.fetchCollection(); }, @@ -154,11 +160,7 @@ define(function(require){ */ onResize: function() { - // we don't want to re-initialise for _every_ resize event - if(this.resizeTimer) { - clearTimeout(this.resizeTimer); - } - this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); + this.initPaging(); }, doLazyScroll: function(e) { @@ -171,7 +173,15 @@ define(function(require){ 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' }); 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() { From db34b77e29bd649d7237e558608f236ed8b6a9a2 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 16:42:53 +0000 Subject: [PATCH 31/94] Rename/fix function --- .../views/assetManagementModalView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 6ab909531c..3c043f03e2 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -23,7 +23,7 @@ define(function(require) { if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { this.setupImageAutofillButton(); } - this.resizeAssetPanels(); + this.resizePanels(); }, setupSubViews: function() { @@ -48,12 +48,10 @@ define(function(require) { 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); + resizePanels: function() { + var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); + this.$('.asset-management-assets-container').height(actualHeight); + this.$('.asset-management-preview-container').height(actualHeight); }, onAssetClicked: function(model) { From efdecfc8dbc5940577e4dfc561ce91ea8beff32b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 16:43:22 +0000 Subject: [PATCH 32/94] Fix issues with whitespace --- .../views/assetManagementModalView.js | 151 ++++++++---------- 1 file changed, 71 insertions(+), 80 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 3c043f03e2..6239cbffd1 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -1,83 +1,74 @@ // 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.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 - 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}); - }, - - resizePanels: function() { - var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); - 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 + 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 }); + }, + + resizePanels: function() { + var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); + this.$('.asset-management-assets-container').height(actualHeight); + this.$('.asset-management-preview-container').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; }); From 2f3a0d838210d69637bc55a4c40c037f0d986f90 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 16:56:54 +0000 Subject: [PATCH 33/94] Fix asset collection page size calculations --- .../views/assetManagementCollectionView.js | 6 +++--- .../views/assetManagementModalView.js | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index a0a15ca5e1..1bc5533734 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -47,8 +47,8 @@ define(function(require){ initPaging: function() { this.resetCollection(_.bind(function(collection) { - var containerHeight = $(window).height() - this.$el.offset().top; - var containerWidth = this.$el.width(); + 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); @@ -178,7 +178,7 @@ define(function(require){ remove: function() { $('.asset-management-assets-container').off('scroll', this._doLazyScroll); $(window).on('resize', this._onResize); - + OriginView.prototype.remove.apply(this, arguments); } diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 6239cbffd1..6885f17775 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -32,7 +32,11 @@ define(function(require) { 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); + var view = new AssetManagementCollectionView({ + collection: this.collection, + search: this.search + }); + this.$('.asset-management-assets-container-inner').append(view.$el); }, setupFilterAndSearchView: function() { @@ -45,8 +49,13 @@ define(function(require) { resizePanels: function() { var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); + 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) { From d9210f7b0825188659a16d985a66671686102469 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 17:47:44 +0000 Subject: [PATCH 34/94] Hide modal scrollbar for asset management --- frontend/src/modules/modal/less/modal.less | 2 +- frontend/src/modules/modal/views/modalView.js | 4 ++++ frontend/src/modules/scaffold/views/scaffoldAssetView.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) 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..0684ec4288 100644 --- a/frontend/src/modules/modal/views/modalView.js +++ b/frontend/src/modules/modal/views/modalView.js @@ -16,6 +16,7 @@ define(function(require) { var defaults = { _shouldShowCancelButton: true, _shouldShowDoneButton: true, + _shouldShowScrollbar: true, _shouldDisableCancelButton: false, _shouldDisableDoneButton: false } @@ -48,6 +49,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/scaffold/views/scaffoldAssetView.js b/frontend/src/modules/scaffold/views/scaffoldAssetView.js index 79aa971020..be0adaeee6 100644 --- a/frontend/src/modules/scaffold/views/scaffoldAssetView.js +++ b/frontend/src/modules/scaffold/views/scaffoldAssetView.js @@ -143,6 +143,7 @@ define(function(require) { Origin.trigger('modal:open', AssetManagementModalView, { collection: new AssetCollection, assetType: this.schema.fieldType, + _shouldShowScrollbar: false, onUpdate: function(data) { if (data) { From db14fd90900f4609caf822eb0f2279aa4d4e5e10 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 17:48:11 +0000 Subject: [PATCH 35/94] Remove duplicate listenTo calls --- frontend/src/modules/modal/views/modalView.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/modal/views/modalView.js b/frontend/src/modules/modal/views/modalView.js index 0684ec4288..ed6a83054e 100644 --- a/frontend/src/modules/modal/views/modalView.js +++ b/frontend/src/modules/modal/views/modalView.js @@ -23,13 +23,17 @@ define(function(require) { this.view = options.view; this.options = _.extend(defaults, 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(); }, From 4be4b3a6cf54be626c1a4997e7f3cec187e13f13 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 17:48:23 +0000 Subject: [PATCH 36/94] Fix whitespace --- frontend/src/modules/modal/views/modalView.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/modal/views/modalView.js b/frontend/src/modules/modal/views/modalView.js index ed6a83054e..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,15 +11,14 @@ 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, { @@ -42,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; }, From b7d8f786113d96560a858f22a0bbba8f3fb3c5bc Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Wed, 13 Dec 2017 14:21:37 +0000 Subject: [PATCH 37/94] Editor performance improvements (#1798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename model attributes * Remove unnecessary collections from editor data * Update attribute references * Remove unused functions * Update models to use actual collection item names * Refactor editor menu code to allow for async loading * Stop re-rendering if item already selected * Remove references to unused model attributes * Improve comments * Stop code executing for each item * Refactor get functions * Remove unused bits * Fix various issues * Remove unused imports * Refactor to only call listenTo once * Update ContentModel to return children as array Due to issue with Backbone.Collections and multiple model types * Remove unused cut functionality * Remove unused code * Update component render to work with async code * Handle re-rendering in parent * Make re-render async * Fix model accessor * Remove unused imports * Simplify component delete code * Stop fetchSiblings from returning self * Add data-id to all editorOriginViews for convenience * Removed unused ‘ancestors’ code * Fix page getter * Refactor new block code * Refactor layout code * Make destroy async to ensure we catch any errors * Refactor for brevity * Refactor block rendering for readability * Refactor new article code * Remove sibling fetch * Fix server copy/paste * Remove unused var * Remove unused route * Remove unused sync classes * Allow for proper async rendering in page views * Fix whitespace * Refactor for readability * Remove logs * Remove asset URL helpers As they’re now async… * Fix courseassets * Fix async course validation * Remove noisy warning * Remove log * Fix scope issue * Fix issue with external assets * Move stuff around * Remove spaghetti logic from scaffoldAsset template * Remove log * Remove log * Stop unnecessary 404 errors * Fix courseassets clean-up * Improve helper to allow for iterators which modify the original list * Improve performance * Make content fetches run in parallel * Make helper parallel * Improve page editor rendering time * Remove mandatory fetch from contentModel.initialize * Make sure model is fetched before rendering * Remove unused import * Move async rendering to EditorPageBlockView * Remove client clipboard data Clipboard data now automatically deleted by back-end * Fix issue with hidden blocks * Fix issues * Make content fetches run in parallel * Make helper parallel * Improve page editor rendering time * Remove mandatory fetch from contentModel.initialize * Make sure model is fetched before rendering * Remove unused import * Move async rendering to EditorPageBlockView * Remove client clipboard data Clipboard data now automatically deleted by back-end * Fix issue with hidden blocks * Add mid-render style to editor page blocks * Fix preview with popup blockers * Stop page re-rendering on paste * Remove logs * Handle destroy async * Fix menu layer rendering for new/removed COs * Make rendering series * Fix menu item state restoration * Fix merge issues * Don’t break for components with no supportedLayout * Move async fetch code to index so scaffold can render correctly Added a multi-fetch convenience method * Refactor for readability * Fix var reference * Update error messages --- .../src/core/collections/contentCollection.js | 35 + frontend/src/core/helpers.js | 204 +++--- frontend/src/core/models/articleModel.js | 6 +- frontend/src/core/models/blockModel.js | 6 +- frontend/src/core/models/componentModel.js | 4 +- .../src/core/models/componentTypeModel.js | 2 +- frontend/src/core/models/contentModel.js | 145 ++-- .../src/core/models/contentObjectModel.js | 6 +- frontend/src/core/models/courseModel.js | 2 +- .../views/assetManagementModalNewAssetView.js | 1 - .../article/views/editorArticleEditView.js | 1 - .../editor/block/views/editorBlockEditView.js | 1 - .../views/editorComponentEditSidebarView.js | 11 +- .../views/editorComponentEditView.js | 1 - .../src/modules/editor/contentObject/index.js | 45 +- .../contentObject/views/editorMenuItemView.js | 117 +--- .../views/editorMenuLayerView.js | 102 ++- .../contentObject/views/editorMenuView.js | 196 ++++-- .../views/editorPageArticleView.js | 114 ++-- .../views/editorPageBlockView.js | 182 +++-- .../views/editorPageComponentListItemView.js | 37 +- .../views/editorPageComponentPasteZoneView.js | 3 - .../views/editorPageComponentView.js | 165 ++--- .../contentObject/views/editorPageView.js | 129 ++-- frontend/src/modules/editor/course/index.js | 19 +- .../course/views/editorCourseEditView.js | 28 +- .../views/editorExtensionsEditView.js | 2 - .../modules/editor/global/editorDataLoader.js | 30 +- .../modules/editor/global/less/editor.less | 22 +- .../editor/global/views/editorOriginView.js | 13 +- .../global/views/editorPasteZoneView.js | 18 +- .../modules/editor/global/views/editorView.js | 316 ++++----- .../scaffold/templates/scaffoldAsset.hbs | 130 ++-- .../scaffold/views/scaffoldAssetView.js | 641 +++++++++--------- lib/contentmanager.js | 119 ++-- plugins/content/clipboard/model.schema | 8 +- routes/lang/en-application.json | 6 +- 37 files changed, 1353 insertions(+), 1514 deletions(-) create mode 100644 frontend/src/core/collections/contentCollection.js 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 b41b7bad8b..ec882aafb3 100644 --- a/frontend/src/core/helpers.js +++ b/frontend/src/core/helpers.js @@ -205,21 +205,6 @@ define(function(require){ } }, - 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); @@ -228,21 +213,6 @@ define(function(require){ } }, - 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); @@ -297,68 +267,38 @@ define(function(require){ return success; }, - - validateCourseContent: function(currentCourse) { - // Let's do a standard check for at least one child object - var containsAtLeastOneChild = true; + // checks for at least one child object + validateCourseContent: function(currentCourse, callback) { + var containsAtLeastOneChild = true; var alerts = []; - - function iterateOverChildren(model) { - // Return the function if no children - on components - if(!model._children) return; - - var currentChildren = model.getChildren(); - - // Do validate across each item - if (currentChildren.length == 0) { - containsAtLeastOneChild = false; - - 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 - ); - - return; - } else { - // Go over each child and call validation again - currentChildren.each(function(childModel) { - iterateOverChildren(childModel); - }); + var iterateOverChildren = function(model, index, doneIterator) { + if(!model._childTypes) { + return doneIterator(); } - - } - - iterateOverChildren(currentCourse); - - if(alerts.length > 0) { + model.fetchChildren(function(currentChildren) { + if (currentChildren.length > 0) { + return helpers.forParallelAsync(currentChildren, iterateOverChildren, doneIterator); + } + containsAtLeastOneChild = false; + var children = _.isArray(model._childTypes) ? model._childTypes.join('/') : model._childTypes; + alerts.push(model.get('_type') + " '" + model.get('title') + "' missing " + children); + doneIterator(); + }); + }; + // start recursion + iterateOverChildren(currentCourse, null, function() { var errorMessage = ""; - for(var i = 0, len = alerts.length; i < len; i++) { - errorMessage += "
  • " + alerts[i] + "
  • "; + if(alerts.length > 0) { + for(var i = 0, len = alerts.length; i < len; i++) { + errorMessage += "
  • " + alerts[i] + "
  • "; + } + return callback(new Error(errorMessage)); } - - Origin.Notify.alert({ - type: 'error', - title: Origin.l10n.t('app.validationfailed'), - text: errorMessage, - callback: _.bind(this.validateCourseConfirm, this) - }); - } - - return containsAtLeastOneChild; + callback(null, true); + }); }, - validateCourseConfirm: function(isConfirmed) { - if (isConfirmed) { - Origin.trigger('editor:courseValidation'); - } - }, - 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)) { @@ -366,6 +306,100 @@ define(function(require){ } else { return true; } + }, + + 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); + } + }, + + /** + * Ensures list is iterated in order, even if using async iterator + * @param list Array or Backbone.Collection + * @param func Function to use as iterator. Will be passed item, index and callback function + * @param callback Function to be called on completion + */ + forSeriesAsync: function(list, func, callback) { + if(!list.hasOwnProperty('length') || list.length === 0) { + if(typeof callback === 'function') callback(); + return; + } + // make a copy in case func modifies the original + var listCopy = list.models ? list.models.slice() : list.slice(); + var doneCount = -1; + var _doAsync = function() { + if(++doneCount === listCopy.length) { + if(typeof callback === 'function') callback(); + return; + } + var nextItem = listCopy[doneCount]; + if(!nextItem) { + console.error('Invalid item at', doneCount + ':', nextItem); + } + func(nextItem, doneCount, _doAsync); + }; + _doAsync(); + }, + + /** + * Does a fetch for model in models, and returns the latest data in the + * passed callback + * @param models {Array of Backbone.Models} + * @param callback {Function to call when complete} + */ + multiModelFetch: function(models, callback) { + var collatedData = {}; + helpers.forParallelAsync(models, function(model, index, done) { + model.fetch({ + success: function(data) { + collatedData[index] = data; + done(); + }, + error: function(data) { + console.error('Failed to fetch data for', model.get('_id'), + data.responseText); + done(); + } + }); + }, function doneAll() { + var orderedKeys = Object.keys(collatedData).sort(); + var returnArr = []; + for(var i = 0, count = orderedKeys.length; i < count; i++) { + returnArr.push(collatedData[orderedKeys[i]]); + } + callback(returnArr); + }); } }; 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..8e20ddc836 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, diff --git a/frontend/src/core/models/componentModel.js b/frontend/src/core/models/componentModel.js index 8ab83cedf9..679b8479ca 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: [ 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/modules/assetManagement/views/assetManagementModalNewAssetView.js b/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js index dac2364e8a..29b3b0d043 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; 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 3777705d02..64f4c9c7dd 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,27 @@ 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) { - Helpers.setPageTitle(model, true); - var form = Origin.scaffold.buildForm({ model: model }); - Origin.sidebar.addView(new EditorPageEditSidebarView({ form: form }).$el); - Origin.contentPane.setView(EditorPageEditView, { model: model, form: form }); + data.model = model; + 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) { @@ -65,7 +53,6 @@ define(function(require) { function renderMenuStructure(data) { Origin.trigger('location:title:update', { title: 'Menu editor' }); - 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..fa1e1aff84 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js @@ -9,37 +9,55 @@ 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) { + 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 +66,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 +93,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,7 +106,7 @@ 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.children('.editor-menu-item-inner').attr('data-id', model.get('_id')); if (type === 'page') { @@ -94,7 +114,7 @@ define(function(require) { this.addNewPageArticleAndBlock(model, newMenuItemView); return; } - newMenuItemView.$el.removeClass('syncing').addClass('synced'); + newMenuItemView.$el.removeClass('syncing'); this.setHeight(); }, this) }); @@ -135,13 +155,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 +166,13 @@ 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) + }); + return newMenuItemView; }, @@ -167,6 +190,37 @@ 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() { + + } + }); } }, { template: 'editorMenuLayer' diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuView.js b/frontend/src/modules/editor/contentObject/views/editorMenuView.js index 2e790093e0..215026fc9e 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'); @@ -11,102 +13,131 @@ define(function(require){ preRender: function() { 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); + 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 + }); }, - setupMenuViews: function() { - this.addMenuLayerView(this); - if (!Origin.editor.currentContentObjectId) { + /** + * Renders all menu layers from the current course to the Origin.editor.currentContentObject + */ + renderLayers: function() { + var selectedModel = Origin.editor.currentContentObject; + // no previous state, so should only render the first level + if(!selectedModel) { + this.renderLayer(Origin.editor.data.course); return; } - this.restoreCurrentMenuState(); + // check if we can reuse any existing layers, and only render the new ones + this.getItemHeirarchy(selectedModel, function(hierarchy) { + var index; + var renderedLayers = this.$('.editor-menu-layer'); + for(var i = 0, count = hierarchy.length; i < count; i++) { + if($(renderedLayers[i]).attr('data-parentid') === hierarchy[i].get('_id')) { + index = i+1; + } + } + // we can reuse layers up to 'index', remove the rest + if(index !== undefined) { + hierarchy = hierarchy.slice(index); + var layersToRemove = renderedLayers.slice(index); + for(var i = 0, count = layersToRemove.length; i < count; i++) { + layersToRemove[i].remove(); + } + } + // all items left in hierarchy are new, render these + Helpers.forSeriesAsync(hierarchy, _.bind(function(model, index, callback) { + this.renderLayer(model, callback); + }, this), _.defer(_.bind(function() { + // called after all layers rendered + this.removeSelectedItemStyling(); + this.addSelectedItemStyling(selectedModel.get('_id')); + this.setUpInteraction(); + }, this))); + }); }, /** - * Recursive function which shows the expanded children for a given context model - * @param {Model} A given contextObject model + * Renders a single menu layer */ - 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)); + renderLayer: function(model, callback) { + var menuLayerView = new EditorMenuLayerView({ + _parentId: model.get('_id'), + models: this.contentobjects.where({ _parentId: model.get('_id') }) + }); + $('.editor-menu-inner').append(menuLayerView.$el); + if(typeof callback === 'function') callback(); }, - /** - * 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'); + setUpInteraction: function() { + this.setupDragDrop(); + var $window = $(window); + this.setupHorizontalScroll($window.width(), $window.height()); + this.scrollToElement(); }, - /** - * Restores the current menu state by finding the current element - * then setting it's parent recursively to _isExpanded - */ - restoreCurrentMenuState: function() { - // Find current menu item - var currentSelectedMenuItem = Origin.editor.data.contentObjects.findWhere({ - _id: Origin.editor.currentContentObjectId - }); - currentSelectedMenuItem.set({ _isSelected: true, _isExpanded: true }); - this.setParentElementToSelected(currentSelectedMenuItem); + 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); + } + }, + + removeSelectedItemStyling: function() { + this.$('.editor-menu-item').removeClass('selected'); }, /** - * This is triggered when an item is clicked + * Generates an array with the inheritence line from a given contentobject to the current course + * @param {Model} contentModel + * @return {Array} */ - updateSelectedItem: function(view) { - // store the ID of the currently selected contentObject - Origin.editor.currentContentObjectId = view.model.get('_id'); - - if(view.model.get('_type') === 'menu') { - this.addMenuLayerView(view); - return; + getItemHeirarchy: function(model, done) { + var hierarchy = []; + if(model.get('_type') === 'menu') { + hierarchy.push(model); } - this.scrollToElement(); + 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()); + }); }, - /** - * 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'); - - if(parentId === Origin.editor.data.course.get('_id')) { + onSelectedItemChanged: function(model) { + if(model.get('_id') === Origin.editor.currentContentObject && Origin.editor.currentContentObject.get('_id')) { return; } - var parentModel = Origin.editor.data.contentObjects.findWhere({ _id: parentId }); - parentModel.set('_isExpanded', true); - - this.setParentElementToSelected(parentModel); + Origin.editor.currentContentObject = model; + this.renderLayers(); }, setupHorizontalScroll: function(windowWidth, windowHeight) { @@ -138,15 +169,15 @@ 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 }); + var currentModel = this.contentobjects.findWhere({ _id: id }); currentModel.save({ _sortOrder: sortOrder, _parentId: parentId }, { patch: true }); currentModel.set('_isDragging', false); - }, + }, this), over: function(event, ui) { $(event.target).closest('.editor-menu-layer').attr('data-over', true); }, @@ -158,6 +189,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..d6a3c8bdaf 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'); @@ -27,10 +24,6 @@ define(function(require){ postRender: function() { this.addBlockViews(); this.setupDragDrop(); - _.defer(_.bind(function(){ - this.trigger('articleView:postRender'); - Origin.trigger('pageView:itemRendered'); - }, this)); }, listenToEvents: function() { @@ -40,8 +33,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 +42,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 +58,63 @@ 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) { + 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 +217,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..00f78622ab 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,12 +9,13 @@ 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', @@ -28,42 +27,70 @@ define(function(require){ 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 +99,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 +117,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 +132,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 +196,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 +217,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 +254,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,17 +276,19 @@ define(function(require){ }, this); }, - swapLayout: function (layout) { - if (layout === 'full') { - return layout; - } - return (layout == 'left') ? 'right' : 'left'; - }, - - 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); + 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' + }); + } + }); } }, { 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 2af692323c..08ecfe2e21 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'); @@ -17,21 +14,33 @@ define(function(require){ 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, { + 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); + }; + originEvents['editorView:moveArticle:' + id] = this.render; + originEvents['editorView:pasted:' + id] = this.onPaste; + this.listenTo(Origin, originEvents); + }, + + render: function() { + var returnVal = EditorOriginView.prototype.render.apply(this, arguments); + + this.addArticleViews(); + + return returnVal; + }, + + postRender: function() { + this.resize(); }, resize: function() { @@ -41,43 +50,10 @@ define(function(require){ }, 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')); - }); - - var blocks = _.filter(Origin.editor.data.blocks.models, function (block) { - return _.contains(articleList, block.get('_parentId')); - }); - - _.each(blocks, function(block) { - blockList.push(block.get('_id')); - }); - - var components = _.filter(Origin.editor.data.components.models, function(component) { - return _.contains(blockList, component.get('_parentId')); - }); - - this.childrenCount = articles.length + blocks.length + components.length; - }, - evaluateChildStatus: function() { this.childrenRenderedCount++; }, - postRender: function() { - this.addArticleViews(); - - _.defer(_.bind(function(){ - this.resize(); - this.trigger('pageView:postRender'); - this.setViewToReady(); - }, this)); - }, - addArticleViews: function() { this.$('.page-articles').empty(); Origin.trigger('editorPageView:removePageSubViews'); @@ -89,19 +65,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,36 +96,29 @@ 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' - }); - - newPageArticleModel.save(null, { + }, { + success: _.bind(function(model, response, options) { + var articleView = this.addArticleView(model); + 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); - var newArticleView = _this.addArticleView(newPageArticleModel); - newArticleView.$el.removeClass('syncing').addClass('synced'); - newArticleView.addBlock(); } }); }, @@ -165,9 +145,18 @@ define(function(require){ Origin.trigger('contextMenu:open', fakeView, event); }, - onCutArticle: function(view) { - this.once('pageView:postRender', view.showPasteZones); - this.render(); + 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' + }); + } + }); } }, { template: 'editorPage' diff --git a/frontend/src/modules/editor/course/index.js b/frontend/src/modules/editor/course/index.js index d256091721..3a54ea890d 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,13 +14,13 @@ define(function(require) { Origin.on('editor:course', renderCourseEdit); function renderCourseEdit() { - (new CourseModel({ _id: Origin.location.route1 })).fetch({ - success: function(model) { - Helpers.setPageTitle({ title: Origin.l10n.t('app.editorsettings') }); - 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) { + EditorHelpers.setPageTitle({ title: Origin.l10n.t('app.editorsettings') }); + var form = Origin.scaffold.buildForm({ model: courseModel }); + Origin.contentPane.setView(EditorCourseEditView, { model: courseModel, form: form }); + Origin.sidebar.addView(new EditorCourseEditSidebarView({ form: form }).$el); }); } @@ -28,7 +29,7 @@ define(function(require) { title: Origin.l10n.t('app.placeholdernewcourse'), displayTitle: Origin.l10n.t('app.placeholdernewcourse') }); - Helpers.setPageTitle({ title: Origin.l10n.t('app.editornew') }); + EditorHelpers.setPageTitle({ title: Origin.l10n.t('app.editornew') }); var form = Origin.scaffold.buildForm({ model: model }); Origin.contentPane.setView(EditorCourseEditView, { model: model, 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..44c37d598d 100644 --- a/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js +++ b/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js @@ -2,8 +2,6 @@ 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 EditorExtensionsEditView = EditorOriginView.extend({ diff --git a/frontend/src/modules/editor/global/editorDataLoader.js b/frontend/src/modules/editor/global/editorDataLoader.js index 9a9635d032..6a1fdaf64e 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 }; // 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..15bc44395f 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 + '/' + data.zipName + '/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/scaffold/templates/scaffoldAsset.hbs b/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs index 73e5b4e089..bf40b12dc8 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}} -