From d013352806d7756565f88b939eca5f8d27e2dd31 Mon Sep 17 00:00:00 2001 From: Aaron Shafovaloff Date: Thu, 27 Jul 2023 14:20:53 -0600 Subject: [PATCH] stop using CJS in shared backbone module test plan: - existing tests pass flag=none Change-Id: Ie8441259a0cd96e5700d0007a62d4dce281ccd54 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/323769 Tested-by: Service Cloud Jenkins Reviewed-by: Isaac Moore QA-Review: Aaron Shafovaloff Product-Review: Aaron Shafovaloff --- .../backbone-ext/Model/dateAttributesSpec.js | 2 +- .../backbone/Backbone.syncWithMultipart.js | 203 ++--- ui/shared/backbone/Collection.js | 569 +++++++------- ui/shared/backbone/Model.js | 177 ++--- ui/shared/backbone/View.js | 733 +++++++++--------- ui/shared/backbone/index.js | 24 +- 6 files changed, 859 insertions(+), 849 deletions(-) diff --git a/spec/coffeescripts/backbone-ext/Model/dateAttributesSpec.js b/spec/coffeescripts/backbone-ext/Model/dateAttributesSpec.js index df5ffa81f76..0d2108e525e 100644 --- a/spec/coffeescripts/backbone-ext/Model/dateAttributesSpec.js +++ b/spec/coffeescripts/backbone-ext/Model/dateAttributesSpec.js @@ -16,7 +16,7 @@ * with this program. If not, see . */ -import Model from '@canvas/backbone/Model' +import {Model} from '@canvas/backbone' QUnit.module('dateAttributes') diff --git a/ui/shared/backbone/Backbone.syncWithMultipart.js b/ui/shared/backbone/Backbone.syncWithMultipart.js index 35429170d34..9e7b0b37fac 100644 --- a/ui/shared/backbone/Backbone.syncWithMultipart.js +++ b/ui/shared/backbone/Backbone.syncWithMultipart.js @@ -17,7 +17,6 @@ // copied from: https://gist.github.com/1998897 -import Backbone from 'backbone' import _ from 'underscore' import $ from 'jquery' import authenticity_token from '@canvas/authenticity-token' @@ -27,110 +26,114 @@ xsslint safeString.identifier iframeId httpMethod xsslint jqueryObject.identifier el */ -Backbone.syncWithoutMultipart = Backbone.sync -Backbone.syncWithMultipart = function (method, model, options) { - // Create a hidden iframe - const iframeId = _.uniqueId('file_upload_iframe_') - const $iframe = $(``).hide() - const dfd = new $.Deferred() +export function patch(Backbone) { + Backbone.syncWithoutMultipart = Backbone.sync + Backbone.syncWithMultipart = function (method, model, options) { + // Create a hidden iframe + const iframeId = _.uniqueId('file_upload_iframe_') + const $iframe = $(``).hide() + const dfd = new $.Deferred() - // Create a hidden form - const httpMethod = { - create: 'POST', - update: 'PUT', - delete: 'DELETE', - read: 'GET', - }[method] + // Create a hidden form + const httpMethod = { + create: 'POST', + update: 'PUT', + delete: 'DELETE', + read: 'GET', + }[method] - function toForm(object, nested, asArray) { - const inputs = _.map(object, (attr, key) => { - if (nested) key = `${nested}[${asArray ? '' : key}]` + function toForm(object, nested, asArray) { + const inputs = _.map(object, (attr, key) => { + if (nested) key = `${nested}[${asArray ? '' : key}]` - if (_.isElement(attr)) { - // leave a copy in the original form, since we're moving it - const $orig = $(attr) - $orig.after($orig.clone(true)) - return attr - } else if (!_.isEmpty(attr) && (_.isArray(attr) || typeof attr === 'object')) { - return toForm(attr, key, _.isArray(attr)) - } else if (!`${key}`.match(/^_/) && attr != null && attr instanceof Date) { - return $('', { - name: key, - value: attr.toISOString(), - })[0] - } else if ( - !`${key}`.match(/^_/) && - attr != null && - typeof attr !== 'object' && - typeof attr !== 'function' - ) { - return $('', { - name: key, - value: attr, - })[0] - } - }) - return _.flatten(inputs) - } - - const $form = $( - `
-
` - ).hide() - - // pass proxyAttachment if the upload is being proxied through canvas (deprecated) - if (options.proxyAttachment) { - $form.prepend( - ` - ` - ) - } - - _.each(toForm(model.toJSON()), el => { - if (!el) return - // s3 expects the file param last - $form[el.name === 'file' ? 'append' : 'prepend'](el) - }) - - $(document.body).prepend($iframe, $form) - - function callback() { - const iframeBody = $iframe[0].contentDocument && $iframe[0].contentDocument.body - - let response = $.parseJSON($(iframeBody).text()) - // in case the form redirects after receiving the upload (API uploads), - // prevent trying to work with an empty response - if (!response) return - - // TODO: Migrate to api v2. Make this check redundant - response = response.objects != null ? response.objects : response - - if (iframeBody.className === 'error') { - if (typeof options.error === 'function') options.error(response) - dfd.reject(response) - } else { - if (typeof options.success === 'function') options.success(response) - dfd.resolve(response) + if (_.isElement(attr)) { + // leave a copy in the original form, since we're moving it + const $orig = $(attr) + $orig.after($orig.clone(true)) + return attr + } else if (!_.isEmpty(attr) && (_.isArray(attr) || typeof attr === 'object')) { + return toForm(attr, key, _.isArray(attr)) + } else if (!`${key}`.match(/^_/) && attr != null && attr instanceof Date) { + return $('', { + name: key, + value: attr.toISOString(), + })[0] + } else if ( + !`${key}`.match(/^_/) && + attr != null && + typeof attr !== 'object' && + typeof attr !== 'function' + ) { + return $('', { + name: key, + value: attr, + })[0] + } + }) + return _.flatten(inputs) } - $iframe.remove() - $form.remove() + const $form = $( + `
+
` + ).hide() + + // pass proxyAttachment if the upload is being proxied through canvas (deprecated) + if (options.proxyAttachment) { + $form.prepend( + ` + ` + ) + } + + _.each(toForm(model.toJSON()), el => { + if (!el) return + // s3 expects the file param last + $form[el.name === 'file' ? 'append' : 'prepend'](el) + }) + + $(document.body).prepend($iframe, $form) + + function callback() { + const iframeBody = $iframe[0].contentDocument && $iframe[0].contentDocument.body + + let response = $.parseJSON($(iframeBody).text()) + // in case the form redirects after receiving the upload (API uploads), + // prevent trying to work with an empty response + if (!response) return + + // TODO: Migrate to api v2. Make this check redundant + response = response.objects != null ? response.objects : response + + if (iframeBody.className === 'error') { + if (typeof options.error === 'function') options.error(response) + dfd.reject(response) + } else { + if (typeof options.success === 'function') options.success(response) + dfd.resolve(response) + } + + $iframe.remove() + $form.remove() + } + + // non-IE + $iframe[0].onload = callback + + $form[0].submit() + return dfd } - // non-IE - $iframe[0].onload = callback - - $form[0].submit() - return dfd -} - -export default Backbone.sync = function (method, model, options) { - return Backbone[ - options && options.multipart ? 'syncWithMultipart' : 'syncWithoutMultipart' - ].apply(this, arguments) + Backbone.sync = function (method, model, options) { + return Backbone[ + options && options.multipart ? 'syncWithMultipart' : 'syncWithoutMultipart' + ].apply(this, arguments) + } } diff --git a/ui/shared/backbone/Collection.js b/ui/shared/backbone/Collection.js index e88d56ee681..929f0d8524a 100644 --- a/ui/shared/backbone/Collection.js +++ b/ui/shared/backbone/Collection.js @@ -19,315 +19,316 @@ /* eslint-disable no-void */ import {extend} from './utils' -import Backbone from 'backbone' import _ from 'underscore' import mixin from './mixin' import DefaultUrlMixin from './DefaultUrlMixin' const slice = [].slice -export default Backbone.Collection = (function (superClass) { - extend(Collection, superClass) +export function patch(Backbone) { + Backbone.Collection = (function (superClass) { + extend(Collection, superClass) - function Collection() { - return Collection.__super__.constructor.apply(this, arguments) - } + function Collection() { + return Collection.__super__.constructor.apply(this, arguments) + } - // # Mixes in objects to a model's definition, being mindful of certain - // # properties (like defaults) that need to be merged also. - // # - // # @param {Object} mixins... - // # @api public - Collection.mixin = function () { - const mixins = arguments.length >= 1 ? slice.call(arguments, 0) : [] - // eslint-disable-next-line prefer-spread - return mixin.apply(null, [this].concat(slice.call(mixins))) - } - - Collection.mixin(DefaultUrlMixin) - - // # Define default options, options passed in to the constructor will - // # overwrite these - Collection.prototype.defaults = { - // # Define some parameters for fetching, they'll be added to the url + // # Mixes in objects to a model's definition, being mindful of certain + // # properties (like defaults) that need to be merged also. // # - // # For example: + // # @param {Object} mixins... + // # @api public + Collection.mixin = function () { + const mixins = arguments.length >= 1 ? slice.call(arguments, 0) : [] + // eslint-disable-next-line prefer-spread + return mixin.apply(null, [this].concat(slice.call(mixins))) + } + + Collection.mixin(DefaultUrlMixin) + + // # Define default options, options passed in to the constructor will + // # overwrite these + Collection.prototype.defaults = { + // # Define some parameters for fetching, they'll be added to the url + // # + // # For example: + // # + // # params: + // # foo: 'bar' + // # baz: [1,2] + // # + // # becomes: + // # + // # ?foo=bar&baz[]=1&baz[]=2 + params: void 0, + // # If using the conventional default URL, define a resource name here or + // # on your model. See `_defaultUrl` for more details. + resourceName: void 0, + // # If using the conventional default URL, define this, or let it fall back + // # to ENV.context_asset_url. See `_defaultUrl` for more details. + contextAssetString: void 0, + } + + // # Defines a key on the options object to be added as an instance property + // # like `model`, `collection`, `el`, etc. on a Backbone.View // # - // # params: - // # foo: 'bar' - // # baz: [1,2] + // # Example: + // # class UserCollection extends Backbone.Collection + // # @optionProperty 'sections' + // # view = new UserCollection + // # sections: new SectionCollection + // # view.sections #=> SectionCollection // # - // # becomes: + // # @param {String} property + // # @api public + Collection.optionProperty = function (property) { + return (this.__optionProperties__ = (this.__optionProperties__ || []).concat([property])) + } + + // # Sets the option properties // # - // # ?foo=bar&baz[]=1&baz[]=2 - params: void 0, - // # If using the conventional default URL, define a resource name here or - // # on your model. See `_defaultUrl` for more details. - resourceName: void 0, - // # If using the conventional default URL, define this, or let it fall back - // # to ENV.context_asset_url. See `_defaultUrl` for more details. - contextAssetString: void 0, - } - - // # Defines a key on the options object to be added as an instance property - // # like `model`, `collection`, `el`, etc. on a Backbone.View - // # - // # Example: - // # class UserCollection extends Backbone.Collection - // # @optionProperty 'sections' - // # view = new UserCollection - // # sections: new SectionCollection - // # view.sections #=> SectionCollection - // # - // # @param {String} property - // # @api public - Collection.optionProperty = function (property) { - return (this.__optionProperties__ = (this.__optionProperties__ || []).concat([property])) - } - - // # Sets the option properties - // # - // # @api private - Collection.prototype.setOptionProperties = function () { - let i, len, property - const ref = this.constructor.__optionProperties__ - const results = [] - for (i = 0, len = ref.length; i < len; i++) { - property = ref[i] - if (this.options[property] !== void 0) { - results.push((this[property] = this.options[property])) - } else { - results.push(void 0) - } - } - return results - } - - // # `options` will be merged into @defaults. Some options will become direct - // # properties of your instance, see `_directPropertyOptions` - Collection.prototype.initialize = function (models, options) { - this.options = _.extend({}, this.defaults, options) - this.setOptionProperties() - return Collection.__super__.initialize.apply(this, arguments) - } - - // # Sets a paramter on @options.params that will be used in `fetch` - Collection.prototype.setParam = function (name, value) { - let base - if ((base = this.options).params == null) { - base.params = {} - } - this.options.params[name] = value - return this.trigger('setParam', name, value) - } - - // # Sets multiple params at once and triggers setParams event - // # - // # @param {Object} params - Collection.prototype.setParams = function (params) { - let name, value - for (name in params) { - value = params[name] - this.setParam(name, value) - } - return this.trigger('setParams', params) - } - - // Deletes a parameter from @options.params - Collection.prototype.deleteParam = function (name) { - let ref - if ((ref = this.options.params) != null) { - delete ref[name] - } - return this.trigger('deleteParam', name) - } - - Collection.prototype.fetch = function (options) { - if (options == null) { - options = {} - } - // TODO: we might want to merge options.data and options.params here instead - if (options.data == null && this.options.params != null) { - options.data = this.options.params - } - return Collection.__super__.fetch.call(this, options).then( - null, - (function (_this) { - return function (xhr) { - return _this.trigger('fetch:fail', xhr) + // # @api private + Collection.prototype.setOptionProperties = function () { + let i, len, property + const ref = this.constructor.__optionProperties__ + const results = [] + for (i = 0, len = ref.length; i < len; i++) { + property = ref[i] + if (this.options[property] !== void 0) { + results.push((this[property] = this.options[property])) + } else { + results.push(void 0) } - })(this) - ) - } - - Collection.prototype.url = function () { - return this._defaultUrl() - } - - Collection.optionProperty('contextAssetString') - - Collection.optionProperty('resourceName') - - // # Overridden to allow recognition of jsonapi.org url style compound - // # documents. - // # - // # These compound documents side load related objects as secondary - // # collections under the linked attribute, rather than embedded within - // # the primary collection's objects. The primary collection is defined - // # by following the jsonapi.org standard. This will look for the first - // # collection after removing reserved keys. - // # - // # To adapt this style to Backbone, we check for any jsonapi.org reserved - // # keys and, if any are found, we extract the first primary collection and - // # pre-process any declared side loads into the embedded format that Backbone - // # expects. - // # - // # Declaring recognized side loads is done through the `sideLoad' property - // # on the collection class. The value of this property is an object whose - // # keys identify the target relation property on the primary objects. The - // # values for those keys can either be `true', a string, or an object. - // # - // # If the value is an object, the foreign key and side loaded collection - // # name are identified by the `foreignKey' and `collection' properties, - // # respectively. Absent properties are inferred from the relation name. - // # - // # A value is `true' is treated the same as an empty object (side load - // # defined, but properties to be inferred). A string value is treated as a - // # hash with a collection name, leaving the foreign key to be inferred. - // # - // # If the value of a foreign key is an array it will be treated as a to_many - // # relationship and load all related documents. - // # - // # For examples, the following are all identical: - // # - // # sideLoad: - // # author: true - // # - // # sideLoad: - // # author: - // # collection: 'authors' - // # - // # sideLoad: - // # author: - // # foreignKey: 'author' - // # collection: 'authors' - // # - // # If the authors are instead contained in the `people' collection, the - // # following can be used interchangeably: - // # - // # sideLoad: - // # author: - // # collection: 'people' - // # - // # sideLoad: - // # author: - // # foreignKey: 'author' - // # collection: 'people' - // # - // # Alternately, if the collection is `authors' and the target relation - // # property is `author', but the foreign key is `person' (such a silly - // # API), you can use: - // # - // # sideLoad: - // # author: - // # foreignKey: 'person' - // # - Collection.prototype.parse = function (response, options) { - if (response == null) { - return Collection.__super__.parse.apply(this, arguments) + } + return results } - const rootMeta = _.pick(response, 'meta', 'links', 'linked') - if (_.isEmpty(rootMeta)) { - return Collection.__super__.parse.apply(this, arguments) + + // # `options` will be merged into @defaults. Some options will become direct + // # properties of your instance, see `_directPropertyOptions` + Collection.prototype.initialize = function (models, options) { + this.options = _.extend({}, this.defaults, options) + this.setOptionProperties() + return Collection.__super__.initialize.apply(this, arguments) } - const collections = _.omit(response, 'meta', 'links', 'linked') - if (_.isEmpty(collections)) { - return Collection.__super__.parse.apply(this, arguments) + + // # Sets a paramter on @options.params that will be used in `fetch` + Collection.prototype.setParam = function (name, value) { + let base + if ((base = this.options).params == null) { + base.params = {} + } + this.options.params[name] = value + return this.trigger('setParam', name, value) } - const collectionKeys = _.keys(collections) - const primaryCollectionKey = _.first(collectionKeys) - const primaryCollection = collections[primaryCollectionKey] - if (primaryCollection == null) { - return Collection.__super__.parse.apply(this, arguments) + + // # Sets multiple params at once and triggers setParams event + // # + // # @param {Object} params + Collection.prototype.setParams = function (params) { + let name, value + for (name in params) { + value = params[name] + this.setParam(name, value) + } + return this.trigger('setParams', params) } - if (collectionKeys.length > 1) { - if (typeof console !== 'undefined' && console !== null) { - // eslint-disable-next-line no-console - if (typeof console.warn === 'function') { + + // Deletes a parameter from @options.params + Collection.prototype.deleteParam = function (name) { + let ref + if ((ref = this.options.params) != null) { + delete ref[name] + } + return this.trigger('deleteParam', name) + } + + Collection.prototype.fetch = function (options) { + if (options == null) { + options = {} + } + // TODO: we might want to merge options.data and options.params here instead + if (options.data == null && this.options.params != null) { + options.data = this.options.params + } + return Collection.__super__.fetch.call(this, options).then( + null, + (function (_this) { + return function (xhr) { + return _this.trigger('fetch:fail', xhr) + } + })(this) + ) + } + + Collection.prototype.url = function () { + return this._defaultUrl() + } + + Collection.optionProperty('contextAssetString') + + Collection.optionProperty('resourceName') + + // # Overridden to allow recognition of jsonapi.org url style compound + // # documents. + // # + // # These compound documents side load related objects as secondary + // # collections under the linked attribute, rather than embedded within + // # the primary collection's objects. The primary collection is defined + // # by following the jsonapi.org standard. This will look for the first + // # collection after removing reserved keys. + // # + // # To adapt this style to Backbone, we check for any jsonapi.org reserved + // # keys and, if any are found, we extract the first primary collection and + // # pre-process any declared side loads into the embedded format that Backbone + // # expects. + // # + // # Declaring recognized side loads is done through the `sideLoad' property + // # on the collection class. The value of this property is an object whose + // # keys identify the target relation property on the primary objects. The + // # values for those keys can either be `true', a string, or an object. + // # + // # If the value is an object, the foreign key and side loaded collection + // # name are identified by the `foreignKey' and `collection' properties, + // # respectively. Absent properties are inferred from the relation name. + // # + // # A value is `true' is treated the same as an empty object (side load + // # defined, but properties to be inferred). A string value is treated as a + // # hash with a collection name, leaving the foreign key to be inferred. + // # + // # If the value of a foreign key is an array it will be treated as a to_many + // # relationship and load all related documents. + // # + // # For examples, the following are all identical: + // # + // # sideLoad: + // # author: true + // # + // # sideLoad: + // # author: + // # collection: 'authors' + // # + // # sideLoad: + // # author: + // # foreignKey: 'author' + // # collection: 'authors' + // # + // # If the authors are instead contained in the `people' collection, the + // # following can be used interchangeably: + // # + // # sideLoad: + // # author: + // # collection: 'people' + // # + // # sideLoad: + // # author: + // # foreignKey: 'author' + // # collection: 'people' + // # + // # Alternately, if the collection is `authors' and the target relation + // # property is `author', but the foreign key is `person' (such a silly + // # API), you can use: + // # + // # sideLoad: + // # author: + // # foreignKey: 'person' + // # + Collection.prototype.parse = function (response, options) { + if (response == null) { + return Collection.__super__.parse.apply(this, arguments) + } + const rootMeta = _.pick(response, 'meta', 'links', 'linked') + if (_.isEmpty(rootMeta)) { + return Collection.__super__.parse.apply(this, arguments) + } + const collections = _.omit(response, 'meta', 'links', 'linked') + if (_.isEmpty(collections)) { + return Collection.__super__.parse.apply(this, arguments) + } + const collectionKeys = _.keys(collections) + const primaryCollectionKey = _.first(collectionKeys) + const primaryCollection = collections[primaryCollectionKey] + if (primaryCollection == null) { + return Collection.__super__.parse.apply(this, arguments) + } + if (collectionKeys.length > 1) { + if (typeof console !== 'undefined' && console !== null) { // eslint-disable-next-line no-console - console.warn( - "Found more then one primary collection, using '" + primaryCollectionKey + "'." - ) + if (typeof console.warn === 'function') { + // eslint-disable-next-line no-console + console.warn( + "Found more then one primary collection, using '" + primaryCollectionKey + "'." + ) + } } } - } - const index = {} - _.each(rootMeta.linked || {}, function (link, key) { - return (index[key] = _.indexBy(link, 'id')) - }) - if (_.isEmpty(index)) { - return Collection.__super__.parse.call(this, primaryCollection, options) - } - _.each(this.sideLoad || {}, function (meta, relation) { - let collection, foreignKey - if (_.isBoolean(meta) && meta) { - meta = {} + const index = {} + _.each(rootMeta.linked || {}, function (link, key) { + return (index[key] = _.indexBy(link, 'id')) + }) + if (_.isEmpty(index)) { + return Collection.__super__.parse.call(this, primaryCollection, options) } - if (_.isString(meta)) { - meta = { - collection: meta, + _.each(this.sideLoad || {}, function (meta, relation) { + let collection, foreignKey + if (_.isBoolean(meta) && meta) { + meta = {} } - } - if (!_.isObject(meta)) { - return - } - foreignKey = meta.foreignKey - collection = meta.collection - if (foreignKey == null) { - foreignKey = '' + relation - } - if (collection == null) { - collection = relation + 's' - } - collection = index[collection] || {} - return _.each(primaryCollection, function (item) { - let related - if (!item.links) { + if (_.isString(meta)) { + meta = { + collection: meta, + } + } + if (!_.isObject(meta)) { return } - related = null - const id = item.links[foreignKey] - if (_.isArray(id)) { - if (_.isEmpty(collection)) { - collection = index[relation] || index[foreignKey] - if (collection == null) { - // eslint-disable-next-line no-throw-literal - throw ( - "Could not find linked collection for '" + - relation + - "' using '" + - foreignKey + - "'." - ) + foreignKey = meta.foreignKey + collection = meta.collection + if (foreignKey == null) { + foreignKey = '' + relation + } + if (collection == null) { + collection = relation + 's' + } + collection = index[collection] || {} + return _.each(primaryCollection, function (item) { + let related + if (!item.links) { + return + } + related = null + const id = item.links[foreignKey] + if (_.isArray(id)) { + if (_.isEmpty(collection)) { + collection = index[relation] || index[foreignKey] + if (collection == null) { + // eslint-disable-next-line no-throw-literal + throw ( + "Could not find linked collection for '" + + relation + + "' using '" + + foreignKey + + "'." + ) + } + } + related = _.map(id, function (pk) { + return collection[pk] + }) + } else { + related = collection[id] + } + if (id != null && related != null) { + item[relation] = related + delete item.links[foreignKey] + if (_.isEmpty(item.links)) { + return delete item.links } } - related = _.map(id, function (pk) { - return collection[pk] - }) - } else { - related = collection[id] - } - if (id != null && related != null) { - item[relation] = related - delete item.links[foreignKey] - if (_.isEmpty(item.links)) { - return delete item.links - } - } + }) }) - }) - return Collection.__super__.parse.call(this, primaryCollection, options) - } + return Collection.__super__.parse.call(this, primaryCollection, options) + } - return Collection -})(Backbone.Collection) + return Collection + })(Backbone.Collection) +} diff --git a/ui/shared/backbone/Model.js b/ui/shared/backbone/Model.js index c84d61fae90..57188711ac1 100644 --- a/ui/shared/backbone/Model.js +++ b/ui/shared/backbone/Model.js @@ -19,109 +19,110 @@ import {extend} from './utils' import mixin from './mixin' import _ from 'underscore' -import Backbone from 'backbone' import './Model/computedAttributes' import './Model/dateAttributes' import './Model/errors' const slice = [].slice -export default Backbone.Model = (function (superClass) { - extend(Model, superClass) +export function patch(Backbone) { + Backbone.Model = (function (superClass) { + extend(Model, superClass) - function Model() { - return Model.__super__.constructor.apply(this, arguments) - } + function Model() { + return Model.__super__.constructor.apply(this, arguments) + } - // Mixes in objects to a model's definition, being mindful of certain - // properties (like defaults) that need to be merged also. - // - // @param {Object} mixins... - // @api public - Model.mixin = function () { - const mixins = arguments.length >= 1 ? slice.call(arguments, 0) : [] - // eslint-disable-next-line prefer-spread - return mixin.apply(null, [this].concat(slice.call(mixins))) - } + // Mixes in objects to a model's definition, being mindful of certain + // properties (like defaults) that need to be merged also. + // + // @param {Object} mixins... + // @api public + Model.mixin = function () { + const mixins = arguments.length >= 1 ? slice.call(arguments, 0) : [] + // eslint-disable-next-line prefer-spread + return mixin.apply(null, [this].concat(slice.call(mixins))) + } - Model.prototype.initialize = function (attributes, options) { - let fn, i, len, ref - Model.__super__.initialize.apply(this, arguments) - this.options = _.extend({}, this.defaults, options) - if (this.__initialize__) { - ref = this.__initialize__ - for (i = 0, len = ref.length; i < len; i++) { - fn = ref[i] - fn.call(this) + Model.prototype.initialize = function (attributes, options) { + let fn, i, len, ref + Model.__super__.initialize.apply(this, arguments) + this.options = _.extend({}, this.defaults, options) + if (this.__initialize__) { + ref = this.__initialize__ + for (i = 0, len = ref.length; i < len; i++) { + fn = ref[i] + fn.call(this) + } } + return this } - return this - } - // Trigger an event indicating an item has started to save. This - // can be used to add a loading icon or trigger another event - // when an model tries to save itself. - // - // For example, inside of the initializer of the model you want - // to show a loading icon you could do something like this - // - // @model.on 'saving', -> console.log "Do something awesome" - // - // @api backbone override - Model.prototype.save = function () { - this.trigger('saving') - return Model.__super__.save.apply(this, arguments) - } - - // Method Summary - // Trigger an event indicating an item has started to delete. This - // can be used to add a loading icon or trigger an event while the - // model is being deleted. - // - // For example, inside of the initializer of the model you want to - // show a loading icon, you could do something like this. - // - // @model.on 'destroying', -> console.log 'Do something awesome' - // - // @api backbone override - Model.prototype.destroy = function () { - this.trigger('destroying') - return Model.__super__.destroy.apply(this, arguments) - } - - // Increment an attribute by 1 (or the specified amount) - Model.prototype.increment = function (key, delta) { - if (delta == null) { - delta = 1 + // Trigger an event indicating an item has started to save. This + // can be used to add a loading icon or trigger another event + // when an model tries to save itself. + // + // For example, inside of the initializer of the model you want + // to show a loading icon you could do something like this + // + // @model.on 'saving', -> console.log "Do something awesome" + // + // @api backbone override + Model.prototype.save = function () { + this.trigger('saving') + return Model.__super__.save.apply(this, arguments) } - return this.set(key, this.get(key) + delta) - } - // Decrement an attribute by 1 (or the specified amount) - Model.prototype.decrement = function (key, delta) { - if (delta == null) { - delta = 1 + // Method Summary + // Trigger an event indicating an item has started to delete. This + // can be used to add a loading icon or trigger an event while the + // model is being deleted. + // + // For example, inside of the initializer of the model you want to + // show a loading icon, you could do something like this. + // + // @model.on 'destroying', -> console.log 'Do something awesome' + // + // @api backbone override + Model.prototype.destroy = function () { + this.trigger('destroying') + return Model.__super__.destroy.apply(this, arguments) } - return this.increment(key, -delta) - } - // Add support for nested attributes on a backbone model. Nested - // attributes are indicated by a . to seperate each level. You get - // get nested attributes by doing the following. - // ie: - // // given {foo: {bar: 'catz'}} - // @get 'foo.bar' // returns catz - // - // @api backbone override - Model.prototype.deepGet = function (property) { - let next, value - const split = property.split('.') - value = this.get(split.shift()) - while ((next = split.shift())) { - value = value[next] + // Increment an attribute by 1 (or the specified amount) + Model.prototype.increment = function (key, delta) { + if (delta == null) { + delta = 1 + } + return this.set(key, this.get(key) + delta) } - return value - } - return Model -})(Backbone.Model) + // Decrement an attribute by 1 (or the specified amount) + Model.prototype.decrement = function (key, delta) { + if (delta == null) { + delta = 1 + } + return this.increment(key, -delta) + } + + // Add support for nested attributes on a backbone model. Nested + // attributes are indicated by a . to seperate each level. You get + // get nested attributes by doing the following. + // ie: + // // given {foo: {bar: 'catz'}} + // @get 'foo.bar' // returns catz + // + // @api backbone override + Model.prototype.deepGet = function (property) { + let next, value + const split = property.split('.') + value = this.get(split.shift()) + while ((next = split.shift())) { + value = value[next] + } + return value + } + + return Model + })(Backbone.Model) +} diff --git a/ui/shared/backbone/View.js b/ui/shared/backbone/View.js index 33703ecd2cb..3e2f5269fd9 100644 --- a/ui/shared/backbone/View.js +++ b/ui/shared/backbone/View.js @@ -20,416 +20,415 @@ import {extend} from './utils' import $ from 'jquery' -import Backbone from 'backbone' import _ from 'underscore' import htmlEscape from 'html-escape' import mixin from './mixin' const slice = [].slice -// Extends Backbone.View on top of itself to be 100X more useful -Backbone.View = (function (superClass) { - extend(View, superClass) +export function patch(Backbone) { + // Extends Backbone.View on top of itself to be 100X more useful + Backbone.View = (function (superClass) { + extend(View, superClass) - function View() { - this.renderView = this.renderView.bind(this) - this.createBindings = this.createBindings.bind(this) - this.render = this.render.bind(this) - return View.__super__.constructor.apply(this, arguments) - } - - // Define default options, options passed in to the view will overwrite these - // - // @api public - View.prototype.defaults = {} - - // Configures elements to cache after render. Keys are css selector strings, - // values are the name of the property to store on the instance. - // - // Example: - // - // class FooView extends Backbone.View - // els: - // '.toolbar': '$toolbar' - // '#main': '$main' - // - // @api public - View.prototype.els = null - - // Defines a key on the options object to be added as an instance property - // like `model`, `collection`, `el`, etc. - // - // Example: - // class SomeView extends Backbone.View - // @optionProperty 'foo' - // view = new SomeView foo: 'bar' - // view.foo #=> 'bar' - // - // @param {String} property - // @api public - View.optionProperty = function (property) { - return (this.__optionProperties__ = (this.__optionProperties__ || []).concat([property])) - } - - // Avoids subclasses that simply add a new template - View.optionProperty('template') - - // Defines a child view that is automatically rendered with the parent view. - // When creating an instance of the parent view the child view is passed in - // as an `optionProperty` on the key `name` and its element will be set to - // the first match of `selector` in the parent view's template. - // - // Example: - // class SearchView - // @child 'inputFilterView', '.filter' - // @child 'collectionView', '.results' - // - // view = new SearchView - // inputFilterView: new InputFilterView - // collectionView: new CollectionView - // view.inputFilterView? #=> true - // view.collectionView? #=> true - // - // @param {String} name - // @param {String} selector - // @api public - View.child = function (name, selector) { - this.optionProperty(name) - if (this.__childViews__ == null) { - this.__childViews__ = [] + function View() { + this.renderView = this.renderView.bind(this) + this.createBindings = this.createBindings.bind(this) + this.render = this.render.bind(this) + return View.__super__.constructor.apply(this, arguments) } - return (this.__childViews__ = this.__childViews__.concat([ - { - name, - selector, - }, - ])) - } - // Initializes the view. - // - // - Stores the view in the element data as 'view' - // - Sets @model.view and @collection.view to itself - // - // @param {Object} options - // @api public - View.prototype.initialize = function (options) { - this.options = _.extend({}, this.defaults, options) - this.setOptionProperties() - this.storeChildrenViews() - this.$el.data('view', this) - this._setViewProperties() - if (this.__initialize__) { - const ref = this.__initialize__ - for (let i = 0, len = ref.length; i < len; i++) { - const fn = ref[i] - fn.call(this) + // Define default options, options passed in to the view will overwrite these + // + // @api public + View.prototype.defaults = {} + + // Configures elements to cache after render. Keys are css selector strings, + // values are the name of the property to store on the instance. + // + // Example: + // + // class FooView extends Backbone.View + // els: + // '.toolbar': '$toolbar' + // '#main': '$main' + // + // @api public + View.prototype.els = null + + // Defines a key on the options object to be added as an instance property + // like `model`, `collection`, `el`, etc. + // + // Example: + // class SomeView extends Backbone.View + // @optionProperty 'foo' + // view = new SomeView foo: 'bar' + // view.foo #=> 'bar' + // + // @param {String} property + // @api public + View.optionProperty = function (property) { + return (this.__optionProperties__ = (this.__optionProperties__ || []).concat([property])) + } + + // Avoids subclasses that simply add a new template + View.optionProperty('template') + + // Defines a child view that is automatically rendered with the parent view. + // When creating an instance of the parent view the child view is passed in + // as an `optionProperty` on the key `name` and its element will be set to + // the first match of `selector` in the parent view's template. + // + // Example: + // class SearchView + // @child 'inputFilterView', '.filter' + // @child 'collectionView', '.results' + // + // view = new SearchView + // inputFilterView: new InputFilterView + // collectionView: new CollectionView + // view.inputFilterView? #=> true + // view.collectionView? #=> true + // + // @param {String} name + // @param {String} selector + // @api public + View.child = function (name, selector) { + this.optionProperty(name) + if (this.__childViews__ == null) { + this.__childViews__ = [] } + return (this.__childViews__ = this.__childViews__.concat([ + { + name, + selector, + }, + ])) } - this.attach() - return this - } - // Store all children views for easy access. - // ie: - // @view.children # {@view1, @view2} - // - // @api private - View.prototype.storeChildrenViews = function () { - if (!this.constructor.__childViews__) { - return - } - return (this.children = _.map( - this.constructor.__childViews__, - (function (_this) { - return function (viewObj) { - return _this[viewObj.name] + // Initializes the view. + // + // - Stores the view in the element data as 'view' + // - Sets @model.view and @collection.view to itself + // + // @param {Object} options + // @api public + View.prototype.initialize = function (options) { + this.options = _.extend({}, this.defaults, options) + this.setOptionProperties() + this.storeChildrenViews() + this.$el.data('view', this) + this._setViewProperties() + if (this.__initialize__) { + const ref = this.__initialize__ + for (let i = 0, len = ref.length; i < len; i++) { + const fn = ref[i] + fn.call(this) } - })(this) - )) - } - - // Sets the option properties - // - // @api private - View.prototype.setOptionProperties = function () { - const ref = this.constructor.__optionProperties__ - const results = [] - for (let i = 0, len = ref.length; i < len; i++) { - const property = ref[i] - if (this.options[property] !== void 0) { - results.push((this[property] = this.options[property])) - } else { - results.push(void 0) } + this.attach() + return this } - return results - } - // Renders the view, calls render hooks - // - // @api public - View.prototype.render = function () { - this.renderEl() - this._afterRender() - return this - } - - // Renders the HTML for the element - // - // @api public - View.prototype.renderEl = function () { - if (this.template) { - return this.$el.html(this.template(this.toJSON())) - } - } - - // Caches elements from `els` config - // - // @api private - View.prototype.cacheEls = function () { - if (this.els) { - const ref = this.els - const results = [] - for (const selector in ref) { - const name = ref[selector] - results.push((this[name] = this.$(selector))) + // Store all children views for easy access. + // ie: + // @view.children # {@view1, @view2} + // + // @api private + View.prototype.storeChildrenViews = function () { + if (!this.constructor.__childViews__) { + return } - return results + return (this.children = _.map( + this.constructor.__childViews__, + (function (_this) { + return function (viewObj) { + return _this[viewObj.name] + } + })(this) + )) } - } - // Internal afterRender - // - // @api private - View.prototype._afterRender = function () { - this.cacheEls() - this.createBindings() - if (this.options.views) { - this.renderViews() - } - this.renderChildViews() - return this.afterRender() - } - - // Define in subclasses to add behavior to your view, ie. creating - // datepickers, dialogs, etc. - // - // Example: - // - // class SomeView extends Backbone.View - // els: '.dialog': '$dialog' - // afterRender: -> - // @$dialog.dialog() - // - // @api private - View.prototype.afterRender = function () { - // magic from `mixin` - if (this.__afterRender__) { - const ref = this.__afterRender__ + // Sets the option properties + // + // @api private + View.prototype.setOptionProperties = function () { + const ref = this.constructor.__optionProperties__ const results = [] for (let i = 0, len = ref.length; i < len; i++) { - const fn = ref[i] - results.push(fn.call(this)) + const property = ref[i] + if (this.options[property] !== void 0) { + results.push((this[property] = this.options[property])) + } else { + results.push(void 0) + } } return results } - } - // Define in subclasses to attach your collection/model events - // - // Example: - // - // class SomeView extends Backbone.View - // attach: -> - // @model.on 'change', @render - // - // @api public - View.prototype.attach = function () { - if (this.__attach__) { - const ref = this.__attach__ - const results = [] - for (let i = 0, len = ref.length; i < len; i++) { - const fn = ref[i] - results.push(fn.call(this)) + // Renders the view, calls render hooks + // + // @api public + View.prototype.render = function () { + this.renderEl() + this._afterRender() + return this + } + + // Renders the HTML for the element + // + // @api public + View.prototype.renderEl = function () { + if (this.template) { + return this.$el.html(this.template(this.toJSON())) } - return results } - } - // Defines the locals for the template with intelligent defaults. - // - // Order of defaults, highest priority first: - // - // 1. `@model.present()` - // 2. `@model.toJSON()` - // 3. `@collection.present()` - // 4. `@collection.toJSON()` - // 5. `@options` - // - // Using `present` is encouraged so that when a model or collection is saved - // to the app it doesn't send along non-persistent attributes. - // - // Also adds the view's `cid`. - // - // @api public - View.prototype.toJSON = function () { - const model = this.model || this.collection - const json = model ? (model.present ? model.present() : model.toJSON()) : this.options - json.cid = this.cid - if (window.ENV != null) { - json.ENV = window.ENV + // Caches elements from `els` config + // + // @api private + View.prototype.cacheEls = function () { + if (this.els) { + const ref = this.els + const results = [] + for (const selector in ref) { + const name = ref[selector] + results.push((this[name] = this.$(selector))) + } + return results + } } - return json - } - // Finds, renders, and assigns all child views defined with `View.child`. - // - // @api private - View.prototype.renderChildViews = function () { - let i, len, name, ref1, selector, target - if (!this.constructor.__childViews__) { - return + // Internal afterRender + // + // @api private + View.prototype._afterRender = function () { + this.cacheEls() + this.createBindings() + if (this.options.views) { + this.renderViews() + } + this.renderChildViews() + return this.afterRender() } - const ref = this.constructor.__childViews__ - for (i = 0, len = ref.length; i < len; i++) { - ref1 = ref[i] - name = ref1.name - selector = ref1.selector - if (this[name] == null) { - if (typeof console !== 'undefined' && console !== null) { - // eslint-disable-next-line no-console - if (typeof console.warn === 'function') { + + // Define in subclasses to add behavior to your view, ie. creating + // datepickers, dialogs, etc. + // + // Example: + // + // class SomeView extends Backbone.View + // els: '.dialog': '$dialog' + // afterRender: -> + // @$dialog.dialog() + // + // @api private + View.prototype.afterRender = function () { + // magic from `mixin` + if (this.__afterRender__) { + const ref = this.__afterRender__ + const results = [] + for (let i = 0, len = ref.length; i < len; i++) { + const fn = ref[i] + results.push(fn.call(this)) + } + return results + } + } + + // Define in subclasses to attach your collection/model events + // + // Example: + // + // class SomeView extends Backbone.View + // attach: -> + // @model.on 'change', @render + // + // @api public + View.prototype.attach = function () { + if (this.__attach__) { + const ref = this.__attach__ + const results = [] + for (let i = 0, len = ref.length; i < len; i++) { + const fn = ref[i] + results.push(fn.call(this)) + } + return results + } + } + + // Defines the locals for the template with intelligent defaults. + // + // Order of defaults, highest priority first: + // + // 1. `@model.present()` + // 2. `@model.toJSON()` + // 3. `@collection.present()` + // 4. `@collection.toJSON()` + // 5. `@options` + // + // Using `present` is encouraged so that when a model or collection is saved + // to the app it doesn't send along non-persistent attributes. + // + // Also adds the view's `cid`. + // + // @api public + View.prototype.toJSON = function () { + const model = this.model || this.collection + const json = model ? (model.present ? model.present() : model.toJSON()) : this.options + json.cid = this.cid + if (window.ENV != null) { + json.ENV = window.ENV + } + return json + } + + // Finds, renders, and assigns all child views defined with `View.child`. + // + // @api private + View.prototype.renderChildViews = function () { + let i, len, name, ref1, selector, target + if (!this.constructor.__childViews__) { + return + } + const ref = this.constructor.__childViews__ + for (i = 0, len = ref.length; i < len; i++) { + ref1 = ref[i] + name = ref1.name + selector = ref1.selector + if (this[name] == null) { + if (typeof console !== 'undefined' && console !== null) { // eslint-disable-next-line no-console - console.warn("I need a child view '" + name + "' but one was not provided") + if (typeof console.warn === 'function') { + // eslint-disable-next-line no-console + console.warn("I need a child view '" + name + "' but one was not provided") + } } } + if (!this[name]) { + continue + } + target = this.$(selector) + this[name].setElement(target) + this[name].render() } - if (!this[name]) { - continue - } - target = this.$(selector) - this[name].setElement(target) - this[name].render() + return null } - return null - } - // Binds a `@model` data to the element's html. Whenever the data changes - // the view is updated automatically. The value will be html-escaped by - // default, but the view can define a format method to specify other - // formatting behavior with `@format`. - // - // Example: - // - //
{I will always mirror @model.get('foo') in here}
- // - // @api private + // Binds a `@model` data to the element's html. Whenever the data changes + // the view is updated automatically. The value will be html-escaped by + // default, but the view can define a format method to specify other + // formatting behavior with `@format`. + // + // Example: + // + //
{I will always mirror @model.get('foo') in here}
+ // + // @api private - /* + /* xsslint safeString.method format */ - View.prototype.createBindings = function (_index, _el) { - return this.$('[data-bind]').each( - (function (_this) { - return function (index, el) { - const $el = $(el) - const attribute = $el.data('bind') - return _this.model.on('change:' + attribute, function (model, value) { - return $el.html(_this.format(attribute, value)) - }) - } - })(this) - ) - } - - // Formats bound attributes values before inserting into the element when - // using `data-bind` in the template. - // - // @param {String} attribute - // @param {String} value - // @api private - View.prototype.format = function (attribute, value) { - return htmlEscape(value) - } - - // Use in cases where normal links occur inside elements with events. - // - // Example: - // - // class RecentItemsView - // events: - // 'click .header': 'expand' - // 'click .header a': 'stopPropagation' - // - // @param {$Event} event - // @api public - View.prototype.stopPropagation = function (event) { - return event.stopPropagation() - } - - // Mixes in objects to a view's definition, being mindful of certain - // properties (like events) that need to be merged also. - // - // @param {Object} mixins... - // @api public - View.mixin = function () { - const mixins = arguments.length >= 1 ? slice.call(arguments, 0) : [] - // eslint-disable-next-line prefer-spread - return mixin.apply(null, [this].concat(slice.call(mixins))) - } - - // DEPRECATED - don't use views option, use `child` constructor method - View.prototype.renderViews = function () { - return _.each(this.options.views, this.renderView) - } - - // DEPRECATED - View.prototype.renderView = function (view, selector) { - let target = this.$('#' + selector) - if (!target.length) { - target = this.$('.' + selector) + View.prototype.createBindings = function (_index, _el) { + return this.$('[data-bind]').each( + (function (_this) { + return function (index, el) { + const $el = $(el) + const attribute = $el.data('bind') + return _this.model.on('change:' + attribute, function (model, value) { + return $el.html(_this.format(attribute, value)) + }) + } + })(this) + ) } - view.setElement(target) - view.render() - return this[selector] != null ? this[selector] : (this[selector] = view) - } - View.prototype.hide = function () { - return this.$el.hide() - } - - View.prototype.show = function () { - return this.$el.show() - } - - View.prototype.toggle = function () { - return this.$el.toggle() - } - - // Set view property for attached model/collection objects. If - // @setViewProperties is set to false, view properties will - // not be set. - // - // Example: - // class SampleView extends Backbone.View - // setViewProperties: false - // - // @api private - View.prototype._setViewProperties = function () { - if (this.setViewProperties === false) { - return + // Formats bound attributes values before inserting into the element when + // using `data-bind` in the template. + // + // @param {String} attribute + // @param {String} value + // @api private + View.prototype.format = function (attribute, value) { + return htmlEscape(value) } - if (this.model) { - this.model.view = this - } - if (this.collection) { - this.collection.view = this - } - } - return View -})(Backbone.View) + // Use in cases where normal links occur inside elements with events. + // + // Example: + // + // class RecentItemsView + // events: + // 'click .header': 'expand' + // 'click .header a': 'stopPropagation' + // + // @param {$Event} event + // @api public + View.prototype.stopPropagation = function (event) { + return event.stopPropagation() + } -export default Backbone.View + // Mixes in objects to a view's definition, being mindful of certain + // properties (like events) that need to be merged also. + // + // @param {Object} mixins... + // @api public + View.mixin = function () { + const mixins = arguments.length >= 1 ? slice.call(arguments, 0) : [] + // eslint-disable-next-line prefer-spread + return mixin.apply(null, [this].concat(slice.call(mixins))) + } + + // DEPRECATED - don't use views option, use `child` constructor method + View.prototype.renderViews = function () { + return _.each(this.options.views, this.renderView) + } + + // DEPRECATED + View.prototype.renderView = function (view, selector) { + let target = this.$('#' + selector) + if (!target.length) { + target = this.$('.' + selector) + } + view.setElement(target) + view.render() + return this[selector] != null ? this[selector] : (this[selector] = view) + } + + View.prototype.hide = function () { + return this.$el.hide() + } + + View.prototype.show = function () { + return this.$el.show() + } + + View.prototype.toggle = function () { + return this.$el.toggle() + } + + // Set view property for attached model/collection objects. If + // @setViewProperties is set to false, view properties will + // not be set. + // + // Example: + // class SampleView extends Backbone.View + // setViewProperties: false + // + // @api private + View.prototype._setViewProperties = function () { + if (this.setViewProperties === false) { + return + } + if (this.model) { + this.model.view = this + } + if (this.collection) { + this.collection.view = this + } + } + + return View + })(Backbone.View) +} diff --git a/ui/shared/backbone/index.js b/ui/shared/backbone/index.js index e3f90bc3f83..7b802f5d25b 100644 --- a/ui/shared/backbone/index.js +++ b/ui/shared/backbone/index.js @@ -20,15 +20,21 @@ // if you want Backbone, import 'Backbone' (this file). It will give you // back a Backbone with all of our instructure specific patches to it. -/* eslint-disable import/no-commonjs */ - -// Get the unpatched Backbone -const Backbone = require('backbone') +import Backbone from 'backbone' +import {patch as patch1} from './Backbone.syncWithMultipart' +import {patch as patch2} from './Model' +import {patch as patch3} from './View' +import {patch as patch4} from './Collection' // Apply all of our patches -require('./Backbone.syncWithMultipart') -require('./Model') -require('./View') -require('./Collection') +patch1(Backbone) +patch2(Backbone) +patch3(Backbone) +patch4(Backbone) -module.exports = Backbone +export const syncWithMultipart = Backbone.syncWithMultipart +export const Model = Backbone.Model +export const Collection = Backbone.Collection +export const View = Backbone.View + +export default Backbone