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 d247d76588..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,37 +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' - }); - - var newArticleView = _this.addArticleView(newPageArticleModel); - - 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); - newArticleView.$el.removeClass('syncing').addClass('synced'); - newArticleView.addBlock(); } }); }, @@ -166,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/projects/less/projects.less b/frontend/src/modules/projects/less/projects.less index d336974225..bdfbd872f4 100644 --- a/frontend/src/modules/projects/less/projects.less +++ b/frontend/src/modules/projects/less/projects.less @@ -38,7 +38,7 @@ ul.projects-options { padding: 30px 30px 0px 0px; } -.grid-layout { +.projects-list[data-layout=grid] { .project-list-item, .shared-project-list-item { margin-left: 25px; @@ -74,7 +74,7 @@ ul.projects-options { } } -.list-layout { +.projects-list[data-layout=list] { margin-left:30px; .project-list-item, diff --git a/frontend/src/modules/projects/less/qproject.less b/frontend/src/modules/projects/less/qproject.less index db30d099ab..e474dfc50e 100644 --- a/frontend/src/modules/projects/less/qproject.less +++ b/frontend/src/modules/projects/less/qproject.less @@ -185,21 +185,6 @@ .project-detail-title { margin-bottom:10px; display: inline-block; - - .project-detail-title-inner { - h4 { - height: 36px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - - & span.highlighted { - background-color: #ff0; - } - } - } } .project-details-row { @@ -212,8 +197,3 @@ margin-bottom:4px; } } - -.shared-projects-details-label { - color:#9d9d9d; - font-weight: 600; -} diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index e6ca357a4c..a9d0bd622e 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -1,7 +1,5 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var OriginView = require('core/views/originView'); var ProjectView = require('./projectView'); @@ -9,142 +7,85 @@ define(function(require){ var ProjectsView = OriginView.extend({ className: 'projects', - settings: { - autoRender: true, - preferencesKey: 'dashboard' + supportedLayouts: [ + "grid", + "list" + ], + + postRender: function() { + this.settings.preferencesKey = 'dashboard'; + this.initUserPreferences(); + this.initEventListeners(); + this.initPaging(); }, - preRender: function(options) { - this.setupFilterSettings(); + initEventListeners: function() { + this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); + this._onResize = _.bind(_.debounce(this.onResize, 250), this); this.listenTo(Origin, { - 'window:resize': this.resizeDashboard, - 'dashboard:layout:grid': this.switchLayoutToGrid, - 'dashboard:layout:list': this.switchLayoutToList, - 'dashboard:dashboardSidebarView:filterBySearch': this.filterBySearchInput, - 'dashboard:dashboardSidebarView:filterByTags': this.filterCoursesByTags, - 'dashboard:sidebarFilter:add': this.addTag, - 'dashboard:sidebarFilter:remove': this.removeTag, - // These need to pass in true to re-render the collections - 'dashboard:sort:asc': function() { this.sortAscending(true); }, - 'dashboard:sort:desc': function() { this.sortDescending(true); }, - 'dashboard:sort:updated': function() { this.sortLastUpdated(true); } - }); - - this.listenTo(this.collection, { - 'add': this.appendProjectItem, - 'sync': this.checkIfCollectionIsEmpty + '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'); } }); - }, - - 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; - // set relevant filters as selected - $("a[data-callback='dashboard:layout:grid']").addClass('selected'); - $("a[data-callback='dashboard:sort:asc']").addClass('selected'); - }, + this.supportedLayouts.forEach(function(layout) { + this.listenTo(Origin, 'dashboard:layout:' + layout, function() { this.doLayout(layout); }); + }, this); - 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); - }, + this.listenTo(this.collection, 'add', this.appendProjectItem); - checkIfCollectionIsEmpty: function() { - this.$('.no-projects').toggleClass('display-none', this.collection.length > 0); + $('.contentPane').on('scroll', this._doLazyScroll); }, - 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'); - }, + initUserPreferences: function() { + var prefs = this.getUserPreferences(); - switchLayoutToGrid: function() { - this.getProjectsContainer().removeClass('list-layout').addClass('grid-layout'); - this.setUserPreference('layout','grid'); + this.doLayout(prefs.layout); + this.doSort(prefs.sort, false); + this.doFilter(prefs.search, prefs.tags, false); + // set relevant filters as selected + $("a[data-callback='dashboard:layout:" + prefs.layout + "']").addClass('selected'); + $("a[data-callback='dashboard:sort:" + prefs.sort + "']").addClass('selected'); + // need to refresh this to get latest filters + prefs = this.getUserPreferences(); + Origin.trigger('options:update:ui', prefs); + Origin.trigger('sidebar:update:ui', prefs); }, - sortAscending: function(shouldRenderProjects) { - this.sort = { title: 1 }; - this.setUserPreference('sort','asc'); - if(shouldRenderProjects) this.updateCollection(true); - }, + // Set some default preferences + getUserPreferences: function() { + var prefs = OriginView.prototype.getUserPreferences.apply(this, arguments); - sortDescending: function(shouldRenderProjects) { - this.sort = { title: -1 }; - this.setUserPreference('sort','desc'); - if(shouldRenderProjects) this.updateCollection(true); - }, + if(!prefs.layout) prefs.layout = 'grid'; + if(!prefs.sort) prefs.sort = 'asc'; - sortLastUpdated: function(shouldRenderProjects) { - this.sort = { updatedAt: -1 }; - this.setUserPreference('sort','updated'); - if (shouldRenderProjects) this.updateCollection(true); + return prefs; }, - setupUserPreferences: function() { - // Preserve the user preferences or display default mode - var userPreferences = this.getUserPreferences(); - // Check if the user preferences are list view - // Else if nothing is set or is grid view default to grid view - if (userPreferences && userPreferences.layout === 'list') { - this.switchLayoutToList(); - } else { - this.switchLayoutToGrid(); - } - // 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); + initPaging: function() { + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = -1; } - // Check if sort is set and sort the collection - if (userPreferences && userPreferences.sort === 'desc') { - this.sortDescending(); - } else if (userPreferences && userPreferences.sort === 'updated') { - this.sortLastUpdated(); - } else { - this.sortAscending(); - } - // Once everything has been setup - // refresh the userPreferences object - userPreferences = this.getUserPreferences(); - // Trigger event to update options UI - Origin.trigger('options:update:ui', userPreferences); - Origin.trigger('sidebar:update:ui', userPreferences); - }, - - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.courseLimit += this.courseDenominator; - this.updateCollection(false); + // we need to load one course first to check page size + this.pageSize = 1; + this.resetCollection(_.bind(function(collection) { + var containerHeight = $(window).height()-this.$el.offset().top; + var containerWidth = this.$('.projects-inner').width(); + var itemHeight = $('.project-list-item').outerHeight(true); + var itemWidth = $('.project-list-item').outerWidth(true); + var columns = Math.floor(containerWidth/itemWidth); + var rows = Math.floor(containerHeight/itemHeight); + // columns stack nicely, but need to add extra row if it's not a clean split + if((containerHeight % itemHeight) > 0) rows++; + this.pageSize = columns*rows; + // need another reset to get the actual pageSize number of items + this.resetCollection(this.setViewToReady); + }, this)); }, getProjectsContainer: function() { @@ -152,130 +93,113 @@ define(function(require){ }, emptyProjectsContainer: function() { - // Trigger event to kill zombie views Origin.trigger('dashboard:dashboardView:removeSubViews'); - // Empty collection container this.getProjectsContainer().empty(); }, - updateCollection: function(reset) { - // If removing items, we need to reset our limits - if (reset) { - // Empty container - this.emptyProjectsContainer(); - // Reset fetches cache - this.shouldStopFetches = false; - this.courseLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } - this.search = _.extend(this.search, { tags: { $all: this.tags } }); - // This is set when the fetched amount is equal to the collection length - // Stops any further fetches and HTTP requests - if (this.shouldStopFetches) { + appendProjectItem: function(model) { + var viewClass = model.isEditable() ? ProjectView : SharedProjectView; + this.getProjectsContainer().append(new viewClass({ model: model }).$el); + }, + + convertFilterTextToPattern: function(filterText) { + var pattern = '.*' + filterText.toLowerCase() + '.*'; + return { title: pattern }; + }, + + resetCollection: function(cb) { + this.emptyProjectsContainer(); + this.fetchCount = 0; + this.shouldStopFetches = false; + this.collection.reset(); + this.fetchCollection(cb); + }, + + fetchCollection: function(cb) { + if(this.shouldStopFetches) { return; } + this.isCollectionFetching = true; this.collection.fetch({ - remove: reset, data: { - search: this.search, + search: _.extend(this.search, { tags: { $all: this.tags } }), operators : { - skip: this.courseLimit, - limit: this.courseDenominator, + skip: this.fetchCount, + limit: this.pageSize, sort: this.sort } }, - success: _.bind(function(data) { - // On successful collection fetching set lazy render to enabled - if (this.collectionLength === this.collection.length) { - this.shouldStopFetches = true; - } else { - this.shouldStopFetches = false; - this.collectionLength = this.collection.length; - } + success: _.bind(function(collection, response) { this.isCollectionFetching = false; + this.fetchCount += response.length; + // stop further fetching if this is the last page + if(response.length < this.pageSize) this.shouldStopFetches = true; + + this.$('.no-projects').toggleClass('display-none', this.fetchCount > 0); + if(typeof cb === 'function') cb(collection); }, this) }); }, - appendProjectItem: function(projectModel) { - projectModel.attributes.title=this.highlight(projectModel.attributes.title) - - if (!projectModel.isEditable()) { - this.getProjectsContainer().append(new SharedProjectView({ model: projectModel }).$el); - } else { - this.getProjectsContainer().append(new ProjectView({ model: projectModel }).$el); + doLazyScroll: function(e) { + if(this.isCollectionFetching) { + return; } + var $el = $(e.currentTarget); + var pxRemaining = this.getProjectsContainer().height() - ($el.scrollTop() + $el.height()); + // we're at the bottom, fetch more + if (pxRemaining <= 0) this.fetchCollection(); }, - highlight: function(text) { - var search = this.getUserPreferences().search || ''; - // replace special characters: .*+?|()[]{}\$^ - search.replace(/[.*+?|()\[\]{}\\$^]/g, "\\$&"); - // add the span - return text.replace(new RegExp(search, "gi"), function(term) { - return '' + term + ''; - }); - }, - - addTag: function(filterType) { - // add filter to this.filters - this.tags.push(filterType); - this.filterCollection(); + doLayout: function(layout) { + if(this.supportedLayouts.indexOf(layout) === -1) { + return; + } + this.getProjectsContainer().attr('data-layout', layout); + this.setUserPreference('layout', layout); + }, + + doSort: function(sort, fetch) { + switch(sort) { + case "desc": + this.sort = { title: -1 }; + break; + case "updated": + this.sort = { updatedAt: -1 }; + break; + case "asc": + default: + sort = "asc"; + this.sort = { title: 1 }; + } + this.setUserPreference('sort', sort); + if(fetch !== false) this.resetCollection(); }, - removeTag: function(filterType) { - // remove filter from this.filters - this.tags = _.filter(this.tags, function(item) { return item != filterType; }); - this.filterCollection(); - }, + doFilter: function(text, tags, fetch) { + text = text || ''; + this.filterText = text; + this.search = this.convertFilterTextToPattern(text); + this.setUserPreference('search', text); - filterCollection: function() { - this.search.tags = this.tags.length - ? { $all: this.tags } - : null ; - this.updateCollection(true); - }, + tags = tags || []; + this.tags = _.pluck(tags, 'id'); + this.setUserPreference('tags', tags); - convertFilterTextToPattern: function(filterText) { - var pattern = '.*' + filterText.toLowerCase() + '.*'; - return { title: pattern}; + if(fetch !== false) this.resetCollection(); }, - filterBySearchInput: function (filterText) { - this.filterText = filterText; - this.search = this.convertFilterTextToPattern(filterText); - this.setUserPreference('search', filterText); - this.updateCollection(true); + onResize: function() { + this.initPaging(); }, - filterCoursesByTags: function(tags) { - this.setUserPreference('tags', tags); - this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); - }, + remove: function() { + $('.contentPane').off('scroll', this._doLazyScroll); - setupLazyScrolling: function() { - var $projectContainer = $('.projects'); - var $projectContainerInner = $('.projects-inner'); - // Remove event before attaching - $projectContainer.off('scroll'); - - $projectContainer.on('scroll', _.bind(function() { - var scrollTop = $projectContainer.scrollTop(); - var scrollableHeight = $projectContainerInner.height(); - var containerHeight = $projectContainer.height(); - // If the scroll position of the assets container is - // near the bottom - if ((scrollableHeight-containerHeight) - scrollTop < 30) { - if (!this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } - } - }, this)); + OriginView.prototype.remove.apply(this, arguments); } + }, { template: 'projects' }); diff --git a/frontend/src/modules/scaffold/backboneFormsOverrides.js b/frontend/src/modules/scaffold/backboneFormsOverrides.js index bf18e15b68..522930be80 100644 --- a/frontend/src/modules/scaffold/backboneFormsOverrides.js +++ b/frontend/src/modules/scaffold/backboneFormsOverrides.js @@ -94,6 +94,33 @@ define(function(require) { return parts.join('
    '); }; + Backbone.Form.editors.List.prototype.removeItem = function(item) { + //Confirm delete + var confirmMsg = this.schema.confirmDelete; + + var remove = _.bind(function(isConfirmed) { + if (isConfirmed === false) return; + + var index = _.indexOf(this.items, item); + + this.items[index].remove(); + this.items.splice(index, 1); + + if (item.addEventTriggered) { + this.trigger('remove', this, item.editor); + this.trigger('change', this); + } + + if (!this.items.length && !this.Editor.isAsync) this.addItem(); + }, this); + + if (confirmMsg) { + window.confirm({ title: confirmMsg, type: 'warning', callback: remove }); + } else { + remove(); + } + }; + // Used to setValue with defaults Backbone.Form.editors.Base.prototype.setValue = function(value) { diff --git a/frontend/src/modules/scaffold/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}} -