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 <svc.cloudjenkins@instructure.com>
Reviewed-by: Isaac Moore <isaac.moore@instructure.com>
QA-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
Product-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
This commit is contained in:
Aaron Shafovaloff 2023-07-27 14:20:53 -06:00
parent 1e18d606d4
commit d013352806
6 changed files with 859 additions and 849 deletions

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Model from '@canvas/backbone/Model'
import {Model} from '@canvas/backbone'
QUnit.module('dateAttributes')

View File

@ -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 = $(`<iframe id="${iframeId}" name="${iframeId}"></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 = $(`<iframe id="${iframeId}" name="${iframeId}"></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 $('<input/>', {
name: key,
value: attr.toISOString(),
})[0]
} else if (
!`${key}`.match(/^_/) &&
attr != null &&
typeof attr !== 'object' &&
typeof attr !== 'function'
) {
return $('<input/>', {
name: key,
value: attr,
})[0]
}
})
return _.flatten(inputs)
}
const $form = $(
`<form
enctype='multipart/form-data'
target='${iframeId}'
action='${htmlEscape(options.url || model.url())}'
method='POST'
>
</form>`
).hide()
// pass proxyAttachment if the upload is being proxied through canvas (deprecated)
if (options.proxyAttachment) {
$form.prepend(
`<input type='hidden' name='_method' value='${httpMethod}' />
<input type='hidden' name='authenticity_token' value='${htmlEscape(authenticity_token())}' />`
)
}
_.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 $('<input/>', {
name: key,
value: attr.toISOString(),
})[0]
} else if (
!`${key}`.match(/^_/) &&
attr != null &&
typeof attr !== 'object' &&
typeof attr !== 'function'
) {
return $('<input/>', {
name: key,
value: attr,
})[0]
}
})
return _.flatten(inputs)
}
$iframe.remove()
$form.remove()
const $form = $(
`<form
enctype='multipart/form-data'
target='${iframeId}'
action='${htmlEscape(options.url || model.url())}'
method='POST'
>
</form>`
).hide()
// pass proxyAttachment if the upload is being proxied through canvas (deprecated)
if (options.proxyAttachment) {
$form.prepend(
`<input type='hidden' name='_method' value='${httpMethod}' />
<input type='hidden' name='authenticity_token' value='${htmlEscape(
authenticity_token()
)}' />`
)
}
_.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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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:
//
// <div data-bind="foo">{I will always mirror @model.get('foo') in here}</div>
//
// @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:
//
// <div data-bind="foo">{I will always mirror @model.get('foo') in here}</div>
//
// @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)
}

View File

@ -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