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:
parent
1e18d606d4
commit
d013352806
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue