(function($){
/**
 * @license almond 0.3.3 Copyright jQuery Foundation and other contributors.
 * Released under MIT license, http://github.com/requirejs/almond/LICENSE
 */
//Going sloppy to avoid 'use strict' string cost, but strict practices should
//be followed.
/*global setTimeout: false */

var requirejs, require, define;
(function (undef) {
	var main, req, makeMap, handlers,
		defined = {},
		waiting = {},
		config = {},
		defining = {},
		hasOwn = Object.prototype.hasOwnProperty,
		aps = [].slice,
		jsSuffixRegExp = /\.js$/;

	function hasProp(obj, prop) {
		return hasOwn.call(obj, prop);
	}

	/**
	 * Given a relative module name, like ./something, normalize it to
	 * a real name that can be mapped to a path.
	 * @param {String} name the relative name
	 * @param {String} baseName a real name that the name arg is relative
	 * to.
	 * @returns {String} normalized name
	 */
	function normalize(name, baseName) {
		var nameParts, nameSegment, mapValue, foundMap, lastIndex,
			foundI, foundStarMap, starI, i, j, part, normalizedBaseParts,
			baseParts = baseName && baseName.split("/"),
			map = config.map,
			starMap = (map && map['*']) || {};

		//Adjust any relative paths.
		if (name) {
			name = name.split('/');
			lastIndex = name.length - 1;

			// If wanting node ID compatibility, strip .js from end
			// of IDs. Have to do this here, and not in nameToUrl
			// because node allows either .js or non .js to map
			// to same file.
			if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
				name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
			}

			// Starts with a '.' so need the baseName
			if (name[0].charAt(0) === '.' && baseParts) {
				//Convert baseName to array, and lop off the last part,
				//so that . matches that 'directory' and not name of the baseName's
				//module. For instance, baseName of 'one/two/three', maps to
				//'one/two/three.js', but we want the directory, 'one/two' for
				//this normalization.
				normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
				name = normalizedBaseParts.concat(name);
			}

			//start trimDots
			for (i = 0; i < name.length; i++) {
				part = name[i];
				if (part === '.') {
					name.splice(i, 1);
					i -= 1;
				} else if (part === '..') {
					// If at the start, or previous value is still ..,
					// keep them so that when converted to a path it may
					// still work when converted to a path, even though
					// as an ID it is less than ideal. In larger point
					// releases, may be better to just kick out an error.
					if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') {
						continue;
					} else if (i > 0) {
						name.splice(i - 1, 2);
						i -= 2;
					}
				}
			}
			//end trimDots

			name = name.join('/');
		}

		//Apply map config if available.
		if ((baseParts || starMap) && map) {
			nameParts = name.split('/');

			for (i = nameParts.length; i > 0; i -= 1) {
				nameSegment = nameParts.slice(0, i).join("/");

				if (baseParts) {
					//Find the longest baseName segment match in the config.
					//So, do joins on the biggest to smallest lengths of baseParts.
					for (j = baseParts.length; j > 0; j -= 1) {
						mapValue = map[baseParts.slice(0, j).join('/')];

						//baseName segment has  config, find if it has one for
						//this name.
						if (mapValue) {
							mapValue = mapValue[nameSegment];
							if (mapValue) {
								//Match, update name to the new value.
								foundMap = mapValue;
								foundI = i;
								break;
							}
						}
					}
				}

				if (foundMap) {
					break;
				}

				//Check for a star map match, but just hold on to it,
				//if there is a shorter segment match later in a matching
				//config, then favor over this star map.
				if (!foundStarMap && starMap && starMap[nameSegment]) {
					foundStarMap = starMap[nameSegment];
					starI = i;
				}
			}

			if (!foundMap && foundStarMap) {
				foundMap = foundStarMap;
				foundI = starI;
			}

			if (foundMap) {
				nameParts.splice(0, foundI, foundMap);
				name = nameParts.join('/');
			}
		}

		return name;
	}

	function makeRequire(relName, forceSync) {
		return function () {
			//A version of a require function that passes a moduleName
			//value for items that may need to
			//look up paths relative to the moduleName
			var args = aps.call(arguments, 0);

			//If first arg is not require('string'), and there is only
			//one arg, it is the array form without a callback. Insert
			//a null so that the following concat is correct.
			if (typeof args[0] !== 'string' && args.length === 1) {
				args.push(null);
			}
			return req.apply(undef, args.concat([relName, forceSync]));
		};
	}

	function makeNormalize(relName) {
		return function (name) {
			return normalize(name, relName);
		};
	}

	function makeLoad(depName) {
		return function (value) {
			defined[depName] = value;
		};
	}

	function callDep(name) {
		if (hasProp(waiting, name)) {
			var args = waiting[name];
			delete waiting[name];
			defining[name] = true;
			main.apply(undef, args);
		}

		if (!hasProp(defined, name) && !hasProp(defining, name)) {
			throw new Error('No ' + name);
		}
		return defined[name];
	}

	//Turns a plugin!resource to [plugin, resource]
	//with the plugin being undefined if the name
	//did not have a plugin prefix.
	function splitPrefix(name) {
		var prefix,
			index = name ? name.indexOf('!') : -1;
		if (index > -1) {
			prefix = name.substring(0, index);
			name = name.substring(index + 1, name.length);
		}
		return [prefix, name];
	}

	//Creates a parts array for a relName where first part is plugin ID,
	//second part is resource ID. Assumes relName has already been normalized.
	function makeRelParts(relName) {
		return relName ? splitPrefix(relName) : [];
	}

	/**
	 * Makes a name map, normalizing the name, and using a plugin
	 * for normalization if necessary. Grabs a ref to plugin
	 * too, as an optimization.
	 */
	makeMap = function (name, relParts) {
		var plugin,
			parts = splitPrefix(name),
			prefix = parts[0],
			relResourceName = relParts[1];

		name = parts[1];

		if (prefix) {
			prefix = normalize(prefix, relResourceName);
			plugin = callDep(prefix);
		}

		//Normalize according
		if (prefix) {
			if (plugin && plugin.normalize) {
				name = plugin.normalize(name, makeNormalize(relResourceName));
			} else {
				name = normalize(name, relResourceName);
			}
		} else {
			name = normalize(name, relResourceName);
			parts = splitPrefix(name);
			prefix = parts[0];
			name = parts[1];
			if (prefix) {
				plugin = callDep(prefix);
			}
		}

		//Using ridiculous property names for space reasons
		return {
			f: prefix ? prefix + '!' + name : name, //fullName
			n: name,
			pr: prefix,
			p: plugin
		};
	};

	function makeConfig(name) {
		return function () {
			return (config && config.config && config.config[name]) || {};
		};
	}

	handlers = {
		require: function (name) {
			return makeRequire(name);
		},
		exports: function (name) {
			var e = defined[name];
			if (typeof e !== 'undefined') {
				return e;
			} else {
				return (defined[name] = {});
			}
		},
		module: function (name) {
			return {
				id: name,
				uri: '',
				exports: defined[name],
				config: makeConfig(name)
			};
		}
	};

	main = function (name, deps, callback, relName) {
		var cjsModule, depName, ret, map, i, relParts,
			args = [],
			callbackType = typeof callback,
			usingExports;

		//Use name if no relName
		relName = relName || name;
		relParts = makeRelParts(relName);

		//Call the callback to define the module, if necessary.
		if (callbackType === 'undefined' || callbackType === 'function') {
			//Pull out the defined dependencies and pass the ordered
			//values to the callback.
			//Default to [require, exports, module] if no deps
			deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
			for (i = 0; i < deps.length; i += 1) {
				map = makeMap(deps[i], relParts);
				depName = map.f;

				//Fast path CommonJS standard dependencies.
				if (depName === "require") {
					args[i] = handlers.require(name);
				} else if (depName === "exports") {
					//CommonJS module spec 1.1
					args[i] = handlers.exports(name);
					usingExports = true;
				} else if (depName === "module") {
					//CommonJS module spec 1.1
					cjsModule = args[i] = handlers.module(name);
				} else if (hasProp(defined, depName) ||
						   hasProp(waiting, depName) ||
						   hasProp(defining, depName)) {
					args[i] = callDep(depName);
				} else if (map.p) {
					map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
					args[i] = defined[depName];
				} else {
					throw new Error(name + ' missing ' + depName);
				}
			}

			ret = callback ? callback.apply(defined[name], args) : undefined;

			if (name) {
				//If setting exports via "module" is in play,
				//favor that over return value and exports. After that,
				//favor a non-undefined return value over exports use.
				if (cjsModule && cjsModule.exports !== undef &&
						cjsModule.exports !== defined[name]) {
					defined[name] = cjsModule.exports;
				} else if (ret !== undef || !usingExports) {
					//Use the return value from the function.
					defined[name] = ret;
				}
			}
		} else if (name) {
			//May just be an object definition for the module. Only
			//worry about defining if have a module name.
			defined[name] = callback;
		}
	};

	requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
		if (typeof deps === "string") {
			if (handlers[deps]) {
				//callback in this case is really relName
				return handlers[deps](callback);
			}
			//Just return the module wanted. In this scenario, the
			//deps arg is the module name, and second arg (if passed)
			//is just the relName.
			//Normalize module name, if it contains . or ..
			return callDep(makeMap(deps, makeRelParts(callback)).f);
		} else if (!deps.splice) {
			//deps is a config object, not an array.
			config = deps;
			if (config.deps) {
				req(config.deps, config.callback);
			}
			if (!callback) {
				return;
			}

			if (callback.splice) {
				//callback is an array, which means it is a dependency list.
				//Adjust args if there are dependencies
				deps = callback;
				callback = relName;
				relName = null;
			} else {
				deps = undef;
			}
		}

		//Support require(['a'])
		callback = callback || function () {};

		//If relName is a function, it is an errback handler,
		//so remove it.
		if (typeof relName === 'function') {
			relName = forceSync;
			forceSync = alt;
		}

		//Simulate async callback;
		if (forceSync) {
			main(undef, deps, callback, relName);
		} else {
			//Using a non-zero value because of concern for what old browsers
			//do, and latest browsers "upgrade" to 4 if lower value is used:
			//http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
			//If want a value immediately, use require('id') instead -- something
			//that works in almond on the global level, but not guaranteed and
			//unlikely to work in other AMD implementations.
			setTimeout(function () {
				main(undef, deps, callback, relName);
			}, 4);
		}

		return req;
	};

	/**
	 * Just drops the config on the floor, but returns req in case
	 * the config return value is used.
	 */
	req.config = function (cfg) {
		return req(cfg);
	};

	/**
	 * Expose module registry for debugging and tooling
	 */
	requirejs._defined = defined;

	define = function (name, deps, callback) {
		if (typeof name !== 'string') {
			throw new Error('See almond README: incorrect module build, no module name');
		}

		//This module may not have dependencies
		if (!deps.splice) {
			//deps is not an array, so probably means
			//an object literal or factory function for
			//the value. Adjust args.
			callback = deps;
			deps = [];
		}

		if (!hasProp(defined, name) && !hasProp(waiting, name)) {
			waiting[name] = [name, deps, callback];
		}
	};

	define.amd = {
		jQuery: true
	};
}());

define("vendor/almond", function(){});

/**
 * This is a slightly modified and forward compatible version of the @wordpress/hooks package
 * as included in the Gutenberg feature plugin version 3.8.0
 */
window.llms=window.llms||{};
// use the core hooks if available
if ( 'undefined' !== typeof window.wp && 'undefined' !== typeof window.wp.hooks ) {
	window.llms.hooks = window.wp.hooks;
// otherwise load our own
} else {
	window.llms.hooks=function(n){var r={};function e(t){if(r[t])return r[t].exports;var o=r[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,e),o.l=!0,o.exports}return e.m=n,e.c=r,e.d=function(n,r,t){e.o(n,r)||Object.defineProperty(n,r,{configurable:!1,enumerable:!0,get:t})},e.r=function(n){Object.defineProperty(n,"__esModule",{value:!0})},e.n=function(n){var r=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(r,"a",r),r},e.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},e.p="",e(e.s=209)}({209:function(n,r,e){"use strict";e.r(r);var t=function(n){return"string"!=typeof n||""===n?(console.error("The namespace must be a non-empty string."),!1):!!/^[a-zA-Z][a-zA-Z0-9_.\-\/]*$/.test(n)||(console.error("The namespace can only contain numbers, letters, dashes, periods, underscores and slashes."),!1)};var o=function(n){return"string"!=typeof n||""===n?(console.error("The hook name must be a non-empty string."),!1):/^__/.test(n)?(console.error("The hook name cannot begin with `__`."),!1):!!/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(n)||(console.error("The hook name can only contain numbers, letters, dashes, periods and underscores."),!1)};var i=function(n){return function(r,e,i){var u=arguments.length>3&&void 0!==arguments[3]?arguments[3]:10;if(o(r)&&t(e))if("function"==typeof i)if("number"==typeof u){var c={callback:i,priority:u,namespace:e};if(n[r]){for(var a=n[r].handlers,l=0;l<a.length&&!(a[l].priority>u);)l++;a.splice(l,0,c),(n.__current||[]).forEach(function(n){n.name===r&&n.currentIndex>=l&&n.currentIndex++})}else n[r]={handlers:[c],runs:0};"hookAdded"!==r&&b("hookAdded",r,e,i,u)}else console.error("If specified, the hook priority must be a number.");else console.error("The hook callback must be a function.")}};var u=function(n,r){return function(e,i){if(o(e)&&(r||t(i))){if(!n[e])return 0;var u=0;if(r)u=n[e].handlers.length,n[e]={runs:n[e].runs,handlers:[]};else for(var c=n[e].handlers,a=function(r){c[r].namespace===i&&(c.splice(r,1),u++,(n.__current||[]).forEach(function(n){n.name===e&&n.currentIndex>=r&&n.currentIndex--}))},l=c.length-1;l>=0;l--)a(l);return"hookRemoved"!==e&&b("hookRemoved",e,i),u}}};var c=function(n){return function(r){return r in n}};var a=function(n,r){return function(e){n[e]||(n[e]={handlers:[],runs:0}),n[e].runs++;for(var t=n[e].handlers,o=arguments.length,i=new Array(o>1?o-1:0),u=1;u<o;u++)i[u-1]=arguments[u];if(!t||!t.length)return r?i[0]:void 0;var c={name:e,currentIndex:0};for(n.__current.push(c),n[e]||(n[e]={runs:0,handlers:[]});c.currentIndex<t.length;){var a=t[c.currentIndex].callback.apply(null,i);r&&(i[0]=a),c.currentIndex++}return n.__current.pop(),r?i[0]:void 0}};var l=function(n){return function(){return n.__current&&n.__current.length?n.__current[n.__current.length-1].name:null}};var s=function(n){return function(r){return void 0===r?void 0!==n.__current[0]:!!n.__current[0]&&r===n.__current[0].name}};var d=function(n){return function(r){if(o(r))return n[r]&&n[r].runs?n[r].runs:0}};var f=function(){var n=Object.create(null),r=Object.create(null);return n.__current=[],r.__current=[],{addAction:i(n),addFilter:i(r),removeAction:u(n),removeFilter:u(r),hasAction:c(n),hasFilter:c(r),removeAllActions:u(n,!0),removeAllFilters:u(r,!0),doAction:a(n),applyFilters:a(r,!0),currentAction:l(n),currentFilter:l(r),doingAction:s(n),doingFilter:s(r),didAction:d(n),didFilter:d(r),actions:n,filters:r}};e.d(r,"addAction",function(){return p}),e.d(r,"addFilter",function(){return v}),e.d(r,"removeAction",function(){return m}),e.d(r,"removeFilter",function(){return A}),e.d(r,"hasAction",function(){return _}),e.d(r,"hasFilter",function(){return F}),e.d(r,"removeAllActions",function(){return g}),e.d(r,"removeAllFilters",function(){return y}),e.d(r,"doAction",function(){return b}),e.d(r,"applyFilters",function(){return k}),e.d(r,"currentAction",function(){return x}),e.d(r,"currentFilter",function(){return I}),e.d(r,"doingAction",function(){return w}),e.d(r,"doingFilter",function(){return O}),e.d(r,"didAction",function(){return T}),e.d(r,"didFilter",function(){return j}),e.d(r,"actions",function(){return z}),e.d(r,"filters",function(){return Z}),e.d(r,"createHooks",function(){return f});var h=f(),p=h.addAction,v=h.addFilter,m=h.removeAction,A=h.removeFilter,_=h.hasAction,F=h.hasFilter,g=h.removeAllActions,y=h.removeAllFilters,b=h.doAction,k=h.applyFilters,x=h.currentAction,I=h.currentFilter,w=h.doingAction,O=h.doingFilter,T=h.didAction,j=h.didFilter,z=h.actions,Z=h.filters}});
}
;
define("vendor/wp-hooks", function(){});

/**
 * Returns the WordPress-loaded version of Underscore for use with things that need it and use Require.
 *
 * @return   obj
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'underscore',[],function() {
	return _;
} );

/**
 * Returns the WordPress-loaded version of Backbone for use with things that need it and use Require.
 *
 * @return   obj
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'backbone',[],function() {
	return Backbone;
} );

/**
 * Returns the WordPress-loaded version of Underscore for use with things that need it and use Require.
 *
 * @package LifterLMS/Scripts
 *
 * @since    3.16.0
 * @version  3.16.0
 */

define( 'jquery',[],function() {
	return jQuery;
} );

/*!
* Backbone.CollectionView, v1.3.4
* Copyright (c)2013 Rotunda Software, LLC.
* Distributed under MIT license
* http://github.com/rotundasoftware/backbone-collection-view
*/

( function( root, factory ) {
	// UMD wrapper
	if ( typeof define === 'function' && define.amd ) {
		// AMD
		define( 'vendor/backbone.collectionView',[ 'underscore', 'backbone', 'jquery' ], factory );
	} else if ( typeof exports !== 'undefined' ) {
		// Node/CommonJS
		module.exports = factory( require('underscore' ), require( 'backbone' ), require( 'backbone' ).$ );
	} else {
		// Browser globals
		factory( root._, root.Backbone, ( root.jQuery || root.Zepto || root.$ ) );
	}
}( this, function( _, Backbone, $ ) {
	var mDefaultModelViewConstructor = Backbone.View;

	var kDefaultReferenceBy = "model";

	var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "itemTemplateFunction", "detachedRendering" ];

	var kStylesForEmptyListCaption = {
		"background" : "transparent",
		"border" : "none",
		"box-shadow" : "none"
	};

	Backbone.CollectionView = Backbone.View.extend( {

		tagName : "ul",

		events : {
			"mousedown > li, tbody > tr > td" : "_listItem_onMousedown",
			"dblclick > li, tbody > tr > td" : "_listItem_onDoubleClick",
			"click" : "_listBackground_onClick",
			"click ul.collection-view, table.collection-view" : "_listBackground_onClick",
			"keydown" : "_onKeydown"
		},

		// only used if Backbone.Courier is available
		spawnMessages : {
			"focus" : "focus"
		},

		//only used if Backbone.Courier is available
		passMessages : { "*" : "." },

		// viewOption definitions with default values.
		initializationOptions : [
			{ "collection" : null },
			{ "modelView" : null },
			{ "modelViewOptions" : {} },
			{ "itemTemplate" : null },
			{ "itemTemplateFunction" : null },
			{ "selectable" : true },
			{ "clickToSelect" : true },
			{ "selectableModelsFilter" : null },
			{ "visibleModelsFilter" : null },
			{ "sortableModelsFilter" : null },
			{ "selectMultiple" : false },
			{ "clickToToggle" : false },
			{ "processKeyEvents" : true },
			{ "sortable" : false },
			{ "sortableOptions" : null },
			{ "reuseModelViews" : true },
			{ "detachedRendering" : false },
			{ "emptyListCaption" : null }
		],

		initialize : function( options ) {
			Backbone.ViewOptions.add( this, "initializationOptions" ); // setup the ViewOptions functionality.
			this.setOptions( options ); // and make use of any provided options

			if( ! this.collection ) this.collection = new Backbone.Collection();

			this._hasBeenRendered = false;

			if( this._isBackboneCourierAvailable() ) {
				Backbone.Courier.add( this );
			}

			this.$el.data( "view", this ); // needed for connected sortable lists
			this.$el.addClass( "collection-view collection-list" ); // collection-list is in there for legacy purposes
			if( this.selectable ) this.$el.addClass( "selectable" );

			if( this.selectable && this.processKeyEvents )
				this.$el.attr( "tabindex", 0 ); // so we get keyboard events

			this.selectedItems = [];

			this._updateItemTemplate();

			if( this.collection )
				this._registerCollectionEvents();

			this.viewManager = new ChildViewContainer();
		},

		_onOptionsChanged : function( changedOptions, originalOptions ) {
			var _this = this;
			var rerender = false;

			_.each( _.keys( changedOptions ), function( changedOptionKey ) {
				var newVal = changedOptions[ changedOptionKey ];
				var oldVal = originalOptions[ changedOptionKey ];
				switch( changedOptionKey ) {
					case "collection" :
						if ( newVal !== oldVal ) {
							_this.stopListening( oldVal );
							_this._registerCollectionEvents();
						}
						break;
					case "selectMultiple" :
						if( ! newVal && _this.selectedItems.length > 1 )
							_this.setSelectedModel( _.first( _this.selectedItems ), { by : "cid" } );
						break;
					case "selectable" :
						if( ! newVal && _this.selectedItems.length > 0 )
							_this.setSelectedModels( [] );

						if( newVal && this.processKeyEvents ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
						else _this.$el.removeAttr( "tabindex", 0 );
						break;
					case "sortable" :
						changedOptions.sortable ? _this._setupSortable() : _this.$el.sortable( "destroy" );
						break;
					case "selectableModelsFilter" :
						_this.reapplyFilter( 'selectableModels' );
						break;
					case "sortableOptions" :
						_this.$el.sortable( "destroy" );
						_this._setupSortable();
						break;
					case "sortableModelsFilter" :
						_this.reapplyFilter( 'sortableModels' );
						break;
					case "visibleModelsFilter" :
						_this.reapplyFilter( 'visibleModels' );
						break;
					case "itemTemplate" :
						_this._updateItemTemplate();
						break;
					case "processKeyEvents" :
						if( newVal && this.selectable ) _this.$el.attr( "tabindex", 0 ); // so we get keyboard events
						else _this.$el.removeAttr( "tabindex", 0 );
						break;
					case "modelView" :
						//need to remove all old view instances
						_this.viewManager.each( function( view ) {
							_this.viewManager.remove( view );
							// destroy the View itself
							view.remove();
						} );
						break;
				}
				if( _.contains( kOptionsRequiringRerendering, changedOptionKey ) ) rerender = true;
			} );

			if( this._hasBeenRendered && rerender ) {
				this.render();
			}
		},

		setOption : function( optionName, optionValue ) { // now is merely a wrapper around backbone.viewOptions' setOptions()
			var optionHash = {};
			optionHash[ optionName ] = optionValue;
			this.setOptions( optionHash );
		},

		getSelectedModel : function( options ) {
			return this.selectedItems.length ? _.first( this.getSelectedModels( options ) ) : null;
		},

		getSelectedModels : function ( options ) {
			var _this = this;

			options = _.extend( {}, {
				by : kDefaultReferenceBy
			}, options );

			var referenceBy = options.by;
			var items = [];

			switch( referenceBy ) {
				case "id" :
					_.each( this.selectedItems, function ( item ) {
						items.push( _this.collection.get( item ).id );
					} );
					break;
				case "cid" :
					items = items.concat( this.selectedItems );
					break;
				case "offset" :
					var curLineNumber = 0;

					var itemElements = this._getVisibleItemEls();

					itemElements.each( function() {
						var thisItemEl = $( this );
						if( thisItemEl.is( ".selected" ) )
							items.push( curLineNumber );
						curLineNumber++;
					} );
					break;
				case "model" :
					_.each( this.selectedItems, function ( item ) {
						items.push( _this.collection.get( item ) );
					} );
					break;
				case "view" :
					_.each( this.selectedItems, function ( item ) {
						items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) );
					} );
					break;
				default :
					throw new Error( "Invalid referenceBy option: " + referenceBy );
					break;
			}

			return items;

		},

		setSelectedModels : function( newSelectedItems, options ) {
			if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value";
			if( ! this.selectable && newSelectedItems.length > 0 ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors

			options = _.extend( {}, {
				silent : false,
				by : kDefaultReferenceBy
			}, options );

			var referenceBy = options.by;
			var newSelectedCids = [];

			switch( referenceBy ) {
				case "cid" :
					newSelectedCids = newSelectedItems;
					break;
				case "id" :
					this.collection.each( function( thisModel ) {
						if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid );
					} );
					break;
				case "model" :
					newSelectedCids = _.pluck( newSelectedItems, "cid" );
					break;
				case "view" :
					_.each( newSelectedItems, function( item ) {
						newSelectedCids.push( item.model.cid );
					} );
					break;
				case "offset" :
					var curLineNumber = 0;
					var selectedItems = [];

					var itemElements = this._getVisibleItemEls();
					itemElements.each( function() {
						var thisItemEl = $( this );
						if( _.contains( newSelectedItems, curLineNumber ) )
							newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) );
						curLineNumber++;
					} );
					break;
				default :
					throw new Error( "Invalid referenceBy option: " + referenceBy );
					break;
			}

			var oldSelectedModels = this.getSelectedModels();
			var oldSelectedCids = _.clone( this.selectedItems );

			this.selectedItems = this._convertStringsToInts( newSelectedCids );
			this._validateSelection();

			var newSelectedModels = this.getSelectedModels();

			if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) )
			{
				this._addSelectedClassToSelectedItems( oldSelectedCids );

				if( ! options.silent )
				{
					if( this._isBackboneCourierAvailable() ) {
						this.spawn( "selectionChanged", {
							selectedModels : newSelectedModels,
							oldSelectedModels : oldSelectedModels
						} );
					} else this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels );
				}

				this.updateDependentControls();
			}
		},

		setSelectedModel : function( newSelectedItem, options ) {
			if( ! newSelectedItem && newSelectedItem !== 0 )
				this.setSelectedModels( [], options );
			else
				this.setSelectedModels( [ newSelectedItem ], options );
		},

		getView : function( reference, options ) {
			options = _.extend( {}, {
				by : kDefaultReferenceBy
			}, options );

			switch( options.by ) {
				case "id" :
				case "cid" :
					var model = this.collection.get( reference ) || null;
					return model && this.viewManager.findByModel( model );
					break;
				case "offset" :
					var itemElements = this._getVisibleItemEls();
					return $( itemElements.get( reference ) );
					break;
				case "model" :
					return this.viewManager.findByModel( reference );
					break;
				default :
					throw new Error( "Invalid referenceBy option: " + referenceBy );
					break;
			}
		},

		render : function() {
			var _this = this;

			this._hasBeenRendered = true;

			if( this.selectable ) this._saveSelection();

			var modelViewContainerEl;

			// If collection view element is a table and it has a tbody
			// within it, render the model views inside of the tbody
			modelViewContainerEl = this._getContainerEl();

			var oldViewManager = this.viewManager;
			this.viewManager = new ChildViewContainer();

			// detach each of our subviews that we have already created to represent models
			// in the collection. We are going to re-use the ones that represent models that
			// are still here, instead of creating new ones, so that we don't loose state
			// information in the views.
			oldViewManager.each( function( thisModelView ) {
				// to boost performance, only detach those views that will be sticking around.
				// we won't need the other ones later, so no need to detach them individually.
				if( this.reuseModelViews && this.collection.get( thisModelView.model.cid ) ) {
					thisModelView.$el.detach();
				} else thisModelView.remove();
			}, this );

			modelViewContainerEl.empty();
			var fragmentContainer;

			if( this.detachedRendering )
				fragmentContainer = document.createDocumentFragment();

			this.collection.each( function( thisModel ) {
				var thisModelView = oldViewManager.findByModelCid( thisModel.cid );
				if( ! this.reuseModelViews || _.isUndefined( thisModelView ) ) {
					// if the model view has not already been created on a
					// previous render then create and initialize it now.
					thisModelView = this._createNewModelView( thisModel, this._getModelViewOptions( thisModel ) );
				}

				this._insertAndRenderModelView( thisModelView, fragmentContainer || modelViewContainerEl );
			}, this );

			if( this.detachedRendering )
				modelViewContainerEl.append( fragmentContainer );

			if( this.sortable ) this._setupSortable();

			this._showEmptyListCaptionIfAppropriate();

			if( this._isBackboneCourierAvailable() )
				this.spawn( "render" );
			else this.trigger( "render" );

			if( this.selectable ) {
				this._restoreSelection();
				this.updateDependentControls();
			}

			this.forceRerenderOnNextSortEvent = false;
		},

		_showEmptyListCaptionIfAppropriate : function ( ) {
			this._removeEmptyListCaption();

			if( this.emptyListCaption ) {
				var visibleEls = this._getVisibleItemEls();

				if( visibleEls.length === 0 ) {
					var emptyListString;

					if( _.isFunction( this.emptyListCaption ) )
						emptyListString = this.emptyListCaption();
					else
						emptyListString = this.emptyListCaption;

					var $emptyListCaptionEl;
					var $varEl = $( "<var class='empty-list-caption'>" + emptyListString + "</var>" );

					// need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td)
					if( this._isRenderedAsList() )
						$emptyListCaptionEl = $varEl.wrapAll( "<li class='not-sortable'></li>" ).parent().css( kStylesForEmptyListCaption );
					else
						$emptyListCaptionEl = $varEl.wrapAll( "<tr class='not-sortable'><td colspan='1000'></td></tr>" ).parent().parent().css( kStylesForEmptyListCaption );

					this._getContainerEl().append( $emptyListCaptionEl );
				}
			}
		},

		_removeEmptyListCaption : function( ) {
			if( this._isRenderedAsList() )
				this._getContainerEl().find( "> li > var.empty-list-caption" ).parent().remove();
			else
				this._getContainerEl().find( "> tr > td > var.empty-list-caption" ).parent().parent().remove();
		},

		// Render a single model view in container object "parentElOrDocumentFragment", which is either
		// a documentFragment or a jquery object. optional arg atIndex is not support for document fragments.
		_insertAndRenderModelView : function( modelView, parentElOrDocumentFragment, atIndex ) {
			var thisModelViewWrapped = this._wrapModelView( modelView );

			if( parentElOrDocumentFragment.nodeType === 11 ) // if we are inserting into a document fragment, we need to use the DOM appendChild method
				parentElOrDocumentFragment.appendChild( thisModelViewWrapped.get( 0 ) );
			else {
				var numberOfModelViewsCurrentlyInDOM = parentElOrDocumentFragment.children().length;
				if( ! _.isUndefined( atIndex ) && atIndex >= 0 && atIndex < numberOfModelViewsCurrentlyInDOM )
					// note this.collection.length might be greater than parentElOrDocumentFragment.children().length here
					parentElOrDocumentFragment.children().eq( atIndex ).before( thisModelViewWrapped );
				else {
					// if we are attempting to insert a modelView in an position that is beyond what is currently in the
					// DOM, then make a note that we need to re-render the collection view on the next sort event. If we dont
					// force this re-render, we can end up with modelViews in the wrong order when the collection defines
					// a comparator and multiple models are added at once. See https://github.com/rotundasoftware/backbone.collectionView/issues/69
					if( ! _.isUndefined( atIndex ) && atIndex > numberOfModelViewsCurrentlyInDOM ) this.forceRerenderOnNextSortEvent = true;

					parentElOrDocumentFragment.append( thisModelViewWrapped );
				}
			}

			this.viewManager.add( modelView );

			// we have to render the modelView after it has been put in context, as opposed to in the
			// initialize function of the modelView, because some rendering might be dependent on
			// the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself,
			// it must be in full context in the DOM tree or else the stretch will not behave as intended.
			var renderResult = modelView.render();

			// return false from the view's render function to hide this item
			if( renderResult === false ) {
				thisModelViewWrapped.hide();
				thisModelViewWrapped.addClass( "not-visible" );
			}

			var hideThisModelView = false;
			if( _.isFunction( this.visibleModelsFilter ) )
				hideThisModelView = ! this.visibleModelsFilter( modelView.model );

			if( thisModelViewWrapped.children().length === 1 )
				thisModelViewWrapped.toggle( ! hideThisModelView );
			else modelView.$el.toggle( ! hideThisModelView );

			thisModelViewWrapped.toggleClass( "not-visible", hideThisModelView );

			if( ! hideThisModelView && this.emptyListCaption ) this._removeEmptyListCaption();
		},

		updateDependentControls : function() {
			if( this._isBackboneCourierAvailable() ) {
				this.spawn( "updateDependentControls", {
					selectedModels : this.getSelectedModels()
				} );
			} else this.trigger( "updateDependentControls", this.getSelectedModels() );
		},

		// Override `Backbone.View.remove` to also destroy all Views in `viewManager`
		remove : function() {
			this.viewManager.each( function( view ) {
				view.remove();
			} );

			Backbone.View.prototype.remove.apply( this, arguments );
		},

		reapplyFilter : function( whichFilter ) {
			var _this = this;

			if( ! _.contains( [ "selectableModels", "sortableModels", "visibleModels" ], whichFilter ) ) {
				throw new Error( "Invalid filter identifier supplied to reapplyFilter: " + whichFilter );
			}

			switch( whichFilter ) {
				case "visibleModels":
					_this.viewManager.each( function( thisModelView ) {
						var notVisible = _this.visibleModelsFilter && ! _this.visibleModelsFilter.call( _this, thisModelView.model );

						thisModelView.$el.toggleClass( "not-visible", notVisible );
						if( _this._modelViewHasWrapperLI( thisModelView ) ) {
							thisModelView.$el.closest( "li" ).toggleClass( "not-visible", notVisible ).toggle( ! notVisible );
						} else thisModelView.$el.toggle( ! notVisible );
					} );

					this._showEmptyListCaptionIfAppropriate();
					break;
				case "sortableModels":
					_this.$el.sortable( "destroy" );

					_this.viewManager.each( function( thisModelView ) {
						var notSortable = _this.sortableModelsFilter && ! _this.sortableModelsFilter.call( _this, thisModelView.model );

						thisModelView.$el.toggleClass( "not-sortable", notSortable );
						if( _this._modelViewHasWrapperLI( thisModelView ) ) {
							thisModelView.$el.closest( "li" ).toggleClass( "not-sortable", notSortable );
						}
					} );

					_this._setupSortable();
					break;
				case "selectableModels":
					_this.viewManager.each( function( thisModelView ) {
						var notSelectable = _this.selectableModelsFilter && ! _this.selectableModelsFilter.call( _this, thisModelView.model );

						thisModelView.$el.toggleClass( "not-selectable", notSelectable );
						if( _this._modelViewHasWrapperLI( thisModelView ) ) {
							thisModelView.$el.closest( "li" ).toggleClass( "not-selectable", notSelectable );
						}
					} );

					_this._validateSelection();
					break;
			}
		},

		// A method to remove the view relating to model.
		_removeModelView : function( modelView ) {
			if( this.selectable ) this._saveSelection();

			this.viewManager.remove( modelView ); // Remove the view from the viewManager
			if( this._modelViewHasWrapperLI( modelView ) ) modelView.$el.parent().remove(); // Remove the li wrapper from the DOM
			modelView.remove(); // Remove the view from the DOM and stop listening to events

			if( this.selectable ) this._restoreSelection();

			this._showEmptyListCaptionIfAppropriate();
		},

		_validateSelectionAndRender : function() {
			this._validateSelection();
			this.render();
		},

		_registerCollectionEvents : function() {

			this.listenTo( this.collection, "add", function( model ) {
				var modelView;
				if( this._hasBeenRendered ) {
					modelView = this._createNewModelView( model, this._getModelViewOptions( model ) );
					this._insertAndRenderModelView( modelView, this._getContainerEl(), this.collection.indexOf( model ) );
				}

				if( this._isBackboneCourierAvailable() )
					this.spawn( "add", modelView );
				else this.trigger( "add", modelView );
			} );

			this.listenTo( this.collection, "remove", function( model ) {
				var modelView;

				if( this._hasBeenRendered ) {
					modelView = this.viewManager.findByModelCid( model.cid );
					this._removeModelView( modelView );
				}

				if( this._isBackboneCourierAvailable() )
					this.spawn( "remove" );
				else this.trigger( "remove" );
			} );

			this.listenTo( this.collection, "reset", function() {
				if( this._hasBeenRendered ) this.render();
				if( this._isBackboneCourierAvailable() )
					this.spawn( "reset" );
				else this.trigger( "reset" );
			} );

			// we should not be listening to change events on the model as a default behavior. the models
			// should be responsible for re-rendering themselves if necessary, and if the collection does
			// also need to re-render as a result of a model change, this should be handled by overriding
			// this method. by default the collection view should not re-render in response to model changes
			// this.listenTo( this.collection, "change", function( model ) {
			// 	if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render();
			// 	if( this._isBackboneCourierAvailable() )
			// 		this.spawn( "change", { model : model } );
			// } );

			this.listenTo( this.collection, "sort", function( collection, options ) {
				if( this._hasBeenRendered && ( options.add !== true || this.forceRerenderOnNextSortEvent ) ) this.render();
				if( this._isBackboneCourierAvailable() )
					this.spawn( "sort" );
				else this.trigger( "sort" );
			} );
		},

		_getContainerEl : function() {
			if ( this._isRenderedAsTable() ) {
				// not all tables have a tbody, so we test
				var tbody = this.$el.find( "> tbody" );
				if ( tbody.length > 0 )
					return tbody;
			}
			return this.$el;
		},

		_getClickedItemId : function( theEvent ) {
			var clickedItemId = null;

			// important to use currentTarget as opposed to target, since we could be bubbling
			// an event that took place within another collectionList
			var clickedItemEl = $( theEvent.currentTarget );
			if( clickedItemEl.closest( ".collection-view" ).get(0) !== this.$el.get(0) ) return;

			// determine which list item was clicked. If we clicked in the blank area
			// underneath all the elements, we want to know that too, since in this
			// case we will want to deselect all elements. so check to see if the clicked
			// DOM element is the list itself to find that out.
			var clickedItem = clickedItemEl.closest( "[data-model-cid]" );
			if( clickedItem.length > 0 )
			{
				clickedItemId = clickedItem.attr( "data-model-cid" );
				if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 );
			}

			return clickedItemId;
		},

		_updateItemTemplate : function() {
			var itemTemplateHtml;
			if( this.itemTemplate )
			{
				if( $( this.itemTemplate ).length === 0 )
					throw "Could not find item template from selector: " + this.itemTemplate;

				itemTemplateHtml = $( this.itemTemplate ).html();
			}
			else
				itemTemplateHtml = this.$( ".item-template" ).html();

			if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml );

		},

		_validateSelection : function() {
			// note can't use the collection's proxy to underscore because "cid" is not an attribute,
			// but an element of the model object itself.
			var modelReferenceIds = _.pluck( this.collection.models, "cid" );
			this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems );

			if( _.isFunction( this.selectableModelsFilter ) )
			{
				this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) {
					return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) );
				}, this );
			}
		},

		_saveSelection : function() {
			// save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called.
			if( ! this.selectable ) throw "Attempt to save selection on non-selectable list";
			this.savedSelection = {
				items : _.clone( this.selectedItems ),
				offset : this.getSelectedModel( { by : "offset" } )
			};
		},

		_restoreSelection : function() {
			if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!";

			// reset selectedItems to empty so that we "redraw" all "selected" classes
			// when we set our new selection. We do this because it is likely that our
			// contents have been refreshed, and we have thus lost all old "selected" classes.
			this.setSelectedModels( [], { silent : true } );

			if( this.savedSelection.items.length > 0 )
			{
				// first try to restore the old selected items using their reference ids.
				this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } );

				// all the items with the saved reference ids have been removed from the list.
				// ok. try to restore the selection based on the offset that used to be selected.
				// this is the expected behavior after a item is deleted from a list (i.e. select
				// the line that immediately follows the deleted line).
				if( this.selectedItems.length === 0 )
					this.setSelectedModel( this.savedSelection.offset, { by : "offset" } );

				// Trigger a selection changed if the previously selected items were not all found
				if (this.selectedItems.length !== this.savedSelection.items.length)
				{
					if( this._isBackboneCourierAvailable() ) {
						this.spawn( "selectionChanged", {
							selectedModels : this.getSelectedModels(),
							oldSelectedModels : []
						} );
					} else this.trigger( "selectionChanged", this.getSelectedModels(), [] );
				}
			}
		},

		_addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) {
			if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = [];

			// oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we
			// only have to add / remove the "selected" class from those items that "selected" state has changed.

			var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass;
			itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems );

			_.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) {
				this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" );

				if( this._isRenderedAsList() ) {
					this._getContainerEl().find( "li[data-model-cid=" + thisItemId + "] > *" ).removeClass( "selected" );
				}
			}, this );

			var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems;
			itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass );

			_.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) {
				this._getContainerEl().find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" );

				if( this._isRenderedAsList() ) {
					this._getContainerEl().find( "li[data-model-cid=" + thisItemId + "] > *" ).addClass( "selected" );
				}
			}, this );
		},

		_reorderCollectionBasedOnHTML : function() {

			var _this = this;

			this._getContainerEl().children().each( function() {
				var thisModelCid = $( this ).attr( "data-model-cid" );

				if( thisModelCid )
				{
					// remove the current model and then add it back (at the end of the collection).
					// When we are done looping through all models, they will be in the correct order.
					var thisModel = _this.collection.get( thisModelCid );
					if( thisModel )
					{
						_this.collection.remove( thisModel, { silent : true } );
						_this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } );
					}
				}
			} );

			if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" );
			else this.collection.trigger( "reorder" );

			if( this.collection.comparator ) this.collection.sort();

		},

		_getModelViewConstructor : function( thisModel ) {
			return this.modelView || mDefaultModelViewConstructor;
		},

		_getModelViewOptions : function( thisModel ) {
			var modelViewOptions = this.modelViewOptions;
			if( _.isFunction( modelViewOptions ) ) modelViewOptions = modelViewOptions( thisModel );

			return _.extend( { model : thisModel }, modelViewOptions );
		},

		_createNewModelView : function( model, modelViewOptions ) {
			var modelViewConstructor = this._getModelViewConstructor( model );
			if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model";

			var newModelView = new( modelViewConstructor )( modelViewOptions );
			newModelView.collectionListView = newModelView.collectionView = this;  // collectionListView for legacy

			return newModelView;
		},

		_wrapModelView : function( modelView ) {
			var _this = this;

			// we use items client ids as opposed to real ids, since we may not have a representation
			// of these models on the server
			var modelViewWrapperEl;

			if( this._isRenderedAsTable() ) {
				// if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid
				modelViewWrapperEl = modelView.$el;
				modelView.$el.attr( "data-model-cid", modelView.model.cid );
			}
			else if( this._isRenderedAsList() ) {
				// if we are rendering the collection in a list, we need wrap each item in an <li></li> (if its not already an <li>)
				// and set the data-model-cid
				if( modelView.$el.is( "li" ) ) {
					modelViewWrapperEl = modelView.$el;
					modelView.$el.attr( "data-model-cid", modelView.model.cid );
				} else {
					modelViewWrapperEl = modelView.$el.wrapAll( "<li data-model-cid='" + modelView.model.cid + "'></li>" ).parent();
				}
			}

			if( _.isFunction( this.sortableModelsFilter ) )
				if( ! this.sortableModelsFilter.call( _this, modelView.model ) ) {
					modelViewWrapperEl.addClass( "not-sortable" );
					modelView.$el.addClass( "not-selectable" );
				}

			if( _.isFunction( this.selectableModelsFilter ) )
				if( ! this.selectableModelsFilter.call( _this, modelView.model ) ) {
					modelViewWrapperEl.addClass( "not-selectable" );
					modelView.$el.addClass( "not-selectable" );
				}

			return modelViewWrapperEl;
		},

		_convertStringsToInts : function( theArray ) {
			return _.map( theArray, function( thisEl ) {
				if( ! _.isString( thisEl ) ) return thisEl;
				var thisElAsNumber = parseInt( thisEl, 10 );
				return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl );
			} );
		},

		_containSameElements : function( arrayA, arrayB ) {
			if( arrayA.length != arrayB.length ) return false;
			var intersectionSize = _.intersection( arrayA, arrayB ).length;
			return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length
		},

		_isRenderedAsTable : function() {
			return this.$el.prop( "tagName" ).toLowerCase() === "table";
		},

		_isRenderedAsList : function() {
			return ! this._isRenderedAsTable();
		},

		_modelViewHasWrapperLI : function( modelView ) {
			return this._isRenderedAsList() && ! modelView.$el.is( "li" );
		},

		// Returns the wrapper HTML element for each visible modelView.
		// When rendering in a table context, the returned elements are the $el of each modelView.
		// When rendering in a list context,
		//   If the $el of the modelView is an <li>, the returned elements are the $el of each modelView.
		//   Otherwise, the returned elements are the <li>'s the collectionView wrapped around each modelView $el.
		_getVisibleItemEls : function() {
			var itemElements = [];
			itemElements = this._getContainerEl().find( "> [data-model-cid]:not(.not-visible)" );

			return itemElements;
		},

		_charCodes : {
			upArrow : 38,
			downArrow : 40
		},

		_isBackboneCourierAvailable : function() {
			return !_.isUndefined( Backbone.Courier );
		},

		_setupSortable : function() {
			var sortableOptions = _.extend( {
				axis : "y",
				distance : 10,
				forcePlaceholderSize : true,
				items : this._isRenderedAsTable() ? "> tbody > tr:not(.not-sortable)" : "> li:not(.not-sortable)",
				start : _.bind( this._sortStart, this ),
				change : _.bind( this._sortChange, this ),
				stop : _.bind( this._sortStop, this ),
				receive : _.bind( this._receive, this ),
				over : _.bind( this._over, this )
			}, _.result( this, "sortableOptions" ) );

			this.$el = this.$el.sortable( sortableOptions );
			//this.$el.sortable( "enable" ); // in case it was disabled previously
		},

		_sortStart : function( event, ui ) {
			var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
			if( this._isBackboneCourierAvailable() )
				this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
			else this.trigger( "sortStart", modelBeingSorted );
		},

		_sortChange : function( event, ui ) {
			var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );

			if( this._isBackboneCourierAvailable() )
				this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } );
			else this.trigger( "sortChange", modelBeingSorted );
		},

		_sortStop : function( event, ui ) {
			var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );
			var modelViewContainerEl = this._getContainerEl();
			var newIndex = modelViewContainerEl.children().index( ui.item );

			if( newIndex == -1 && modelBeingSorted ) {
				// the element was removed from this list. can happen if this sortable is connected
				// to another sortable, and the item was dropped into the other sortable.
				this.collection.remove( modelBeingSorted );
			}

			if( ! modelBeingSorted ) return; // something is wacky. we don't mess with this case, preferring to guarantee that we can always provide a reference to the model

			this._reorderCollectionBasedOnHTML();
			this.updateDependentControls();

			if( this._isBackboneCourierAvailable() )
				this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
			else this.trigger( "sortStop", modelBeingSorted, newIndex );
		},

		_receive : function( event, ui ) {

			var senderListEl = ui.sender;
			var senderCollectionListView = senderListEl.data( "view" );
			if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;

			var newIndex = this._getContainerEl().children().index( ui.item );
			var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
			senderCollectionListView.collection.remove( modelReceived );
			this.collection.add( modelReceived, { at : newIndex } );
			modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
			this.setSelectedModel( modelReceived );
		},

		_over : function( event, ui ) {
			// when an item is being dragged into the sortable,
			// hide the empty list caption if it exists
			this._getContainerEl().find( "> var.empty-list-caption" ).hide();
		},

		_onKeydown : function( event ) {
			if( ! this.processKeyEvents ) return true;

			var trap = false;

			if( this.getSelectedModels( { by : "offset" } ).length == 1 )
			{
				// need to trap down and up arrows or else the browser
				// will end up scrolling a autoscroll div.

				var currentOffset = this.getSelectedModel( { by : "offset" } );
				if( event.which === this._charCodes.upArrow && currentOffset !== 0 )
				{
					this.setSelectedModel( currentOffset - 1, { by : "offset" } );
					trap = true;
				}
				else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 )
				{
					this.setSelectedModel( currentOffset + 1, { by : "offset" } );
					trap = true;
				}
			}

			return ! trap;
		},

		_listItem_onMousedown : function( theEvent ) {
			var clickedItemId = this._getClickedItemId( theEvent );

			if( clickedItemId ) {
				var clickedModel = this.collection.get( clickedItemId );
				if( this._isBackboneCourierAvailable() ) {
					var data = {
						clickedModel : clickedModel,
						metaKeyPressed : theEvent.ctrlKey || theEvent.metaKey
					};

					_.each( [ 'preventDefault', 'stopPropagation', 'stopImmediatePropagation' ], function( thisMethod ) {
						data[ thisMethod ] = function() {
							theEvent[ thisMethod ]();
						};
					} );

					this.spawn( "click", data );
				}
				else this.trigger( "click", clickedModel );
			}

			if( ! this.selectable || ! this.clickToSelect ) return;

			if( clickedItemId )
			{
				// Exit if an unselectable item was clicked
				if( _.isFunction( this.selectableModelsFilter ) &&
					! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) )
				{
					return;
				}

				// a selectable list item was clicked
				if( this.selectMultiple && theEvent.shiftKey )
				{
					var firstSelectedItemIndex = -1;

					if( this.selectedItems.length > 0 )
					{
						this.collection.find( function( thisItemModel ) {
							firstSelectedItemIndex++;

							// exit when we find our first selected element
							return _.contains( this.selectedItems, thisItemModel.cid );
						}, this );
					}

					var clickedItemIndex = -1;
					this.collection.find( function( thisItemModel ) {
						clickedItemIndex++;

						// exit when we find the clicked element
						return thisItemModel.cid == clickedItemId;
					}, this );

					var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex;
					var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex );
					var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex );

					var newSelectedItems = [];
					for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ )
						newSelectedItems.push( this.collection.at( thisIndex ).cid );
					this.setSelectedModels( newSelectedItems, { by : "cid" } );

					// shift clicking will usually highlight selectable text, which we do not want.
					// this is a cross browser (hopefully) snippet that deselects all text selection.
					if( document.selection && document.selection.empty )
						document.selection.empty();
					else if(window.getSelection) {
						var sel = window.getSelection();
						if( sel && sel.removeAllRanges )
							sel.removeAllRanges();
					}
				}
				else if( ( this.selectMultiple || _.contains( this.selectedItems, clickedItemId ) ) && ( this.clickToToggle || theEvent.metaKey || theEvent.ctrlKey ) )
				{
					if( _.contains( this.selectedItems, clickedItemId ) )
						this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } );
					else this.setSelectedModels( _.union( this.selectedItems, [clickedItemId] ), { by : "cid" } );
				}
				else
					this.setSelectedModels( [ clickedItemId ], { by : "cid" } );
			}
			else
				// the blank area of the list was clicked
				this.setSelectedModels( [] );

		},

		_listItem_onDoubleClick : function( theEvent ) {

			var clickedItemId = this._getClickedItemId( theEvent );

			if( clickedItemId )
			{
				var clickedModel = this.collection.get( clickedItemId );

				if( this._isBackboneCourierAvailable() )
					this.spawn( "doubleClick", { clickedModel : clickedModel, metaKeyPressed : theEvent.ctrlKey || theEvent.metaKey } );
				else this.trigger( "doubleClick", clickedModel );
			}
		},

		_listBackground_onClick : function( theEvent ) {
			if( ! this.selectable || ! this.clickToSelect ) return;
			if( ! $( theEvent.target ).is( ".collection-view" ) ) return;

			this.setSelectedModels( [] );
		}

	}, {
		setDefaultModelViewConstructor : function( theConstructor ) {
			mDefaultModelViewConstructor = theConstructor;
		}
	});

	/*
	* Backbone.ViewOptions, v0.2.4
	* Copyright (c)2014 Rotunda Software, LLC.
	* Distributed under MIT license
	* http://github.com/rotundasoftware/backbone.viewOptions
	*/

	Backbone.ViewOptions = {};

	Backbone.ViewOptions.add = function( view, optionsDeclarationsProperty ) {
		if( _.isUndefined( optionsDeclarationsProperty ) ) optionsDeclarationsProperty = "options";

		// ****************** Public methods added to view ******************

		view.setOptions = function( options ) {
			var _this = this;
			var optionsThatWereChanged = {};
			var optionsThatWereChangedPreviousValues = {};

			var optionDeclarations = _.result( this, optionsDeclarationsProperty );

			if( ! _.isUndefined( optionDeclarations ) ) {
				var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );

				_.each( normalizedOptionDeclarations, function( thisOptionProperties, thisOptionName ) {
					var thisOptionRequired = thisOptionProperties.required;
					var thisOptionDefaultValue = thisOptionProperties.defaultValue;

					if( thisOptionRequired ) {
						// note we do not throw an error if a required option is not supplied, but it is
						// found on the object itself (due to a prior call of view.setOptions, most likely)

						if( ( ! options || ! _.contains( _.keys( options ), thisOptionName ) ) && _.isUndefined( _this[ thisOptionName ] ) )
							throw new Error( "Required option \"" + thisOptionName + "\" was not supplied." );

						if( options && _.contains( _.keys( options ), thisOptionName ) && _.isUndefined( options[ thisOptionName ] ) )
							throw new Error( "Required option \"" + thisOptionName + "\" can not be set to undefined." );
					}

					// attach the supplied value of this option, or the appropriate default value, to the view object
					if( options && thisOptionName in options && ! _.isUndefined( options[ thisOptionName ] ) ) {
						var oldValue = _this[ thisOptionName ];
						var newValue = options[ thisOptionName ];
						// if this option already exists on the view, and the new value is different,
						// make a note that we will be changing it
						if( ! _.isUndefined( oldValue ) && oldValue !== newValue ) {
							optionsThatWereChangedPreviousValues[ thisOptionName ] = oldValue;
							optionsThatWereChanged[ thisOptionName ] = newValue;
						}
						_this[ thisOptionName ] = newValue;
						// note we do NOT delete the option off the options object here so that
						// multiple views can be passed the same options object without issue.
					}
					else if( _.isUndefined( _this[ thisOptionName ] ) ) {
						// note defaults do not write over any existing properties on the view itself.
						_this[ thisOptionName ] = thisOptionDefaultValue;
					}
				} );
			}

			if( _.keys( optionsThatWereChanged ).length > 0 ) {
				if( _.isFunction( _this.onOptionsChanged ) )
					_this.onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedPreviousValues );
				else if( _.isFunction( _this._onOptionsChanged ) )
					_this._onOptionsChanged( optionsThatWereChanged, optionsThatWereChangedPreviousValues );
			}
		};

		view.getOptions = function() {
			var optionDeclarations = _.result( this, optionsDeclarationsProperty );
			if( _.isUndefined( optionDeclarations ) ) return {};

			var normalizedOptionDeclarations = _normalizeOptionDeclarations( optionDeclarations );
			var optionsNames = _.keys( normalizedOptionDeclarations );

			return _.pick( this, optionsNames );
		};
	};

	// ****************** Private Utility Functions ******************

	function _normalizeOptionDeclarations( optionDeclarations ) {
		// convert our short-hand option syntax (with exclamation marks, etc.)
		// to a simple array of standard option declaration objects.

		var normalizedOptionDeclarations = {};

		if( ! _.isArray( optionDeclarations ) ) throw new Error( "Option declarations must be an array." );

		_.each( optionDeclarations, function( thisOptionDeclaration ) {
			var thisOptionName, thisOptionRequired, thisOptionDefaultValue;

			thisOptionRequired = false;
			thisOptionDefaultValue = undefined;

			if( _.isString( thisOptionDeclaration ) )
				thisOptionName = thisOptionDeclaration;
			else if( _.isObject( thisOptionDeclaration ) ) {
				thisOptionName = _.first( _.keys( thisOptionDeclaration ) );
				if( _.isFunction( thisOptionDeclaration[ thisOptionName ] ) )
					thisOptionDefaultValue = thisOptionDeclaration[ thisOptionName ];
				else
					thisOptionDefaultValue = _.clone( thisOptionDeclaration[ thisOptionName ] );
			}
			else throw new Error( "Each element in the option declarations array must be either a string or an object." );

			if( thisOptionName[ thisOptionName.length - 1 ] === "!" ) {
				thisOptionRequired = true;
				thisOptionName = thisOptionName.slice( 0, thisOptionName.length - 1 );
			}

			normalizedOptionDeclarations[ thisOptionName ] = normalizedOptionDeclarations[ thisOptionName ] || {};
			normalizedOptionDeclarations[ thisOptionName ].required = thisOptionRequired;
			if( ! _.isUndefined( thisOptionDefaultValue ) ) normalizedOptionDeclarations[ thisOptionName ].defaultValue = thisOptionDefaultValue;
		} );

		return normalizedOptionDeclarations;
	}


	// Backbone.BabySitter
	// -------------------
	// v0.0.6
	//
	// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
	// Distributed under MIT license
	//
	// http://github.com/babysitterjs/backbone.babysitter

	// Backbone.ChildViewContainer
	// ---------------------------
	//
	// Provide a container to store, retrieve and
	// shut down child views.

	ChildViewContainer = (function(Backbone, _){

		// Container Constructor
		// ---------------------

		var Container = function(views){
			this._views = {};
			this._indexByModel = {};
			this._indexByCustom = {};
			this._updateLength();

			_.each(views, this.add, this);
		};

		// Container Methods
		// -----------------

		_.extend(Container.prototype, {

			// Add a view to this container. Stores the view
			// by `cid` and makes it searchable by the model
			// cid (and model itself). Optionally specify
			// a custom key to store an retrieve the view.
			add: function(view, customIndex){
				var viewCid = view.cid;

				// store the view
				this._views[viewCid] = view;

				// index it by model
				if (view.model){
					this._indexByModel[view.model.cid] = viewCid;
				}

				// index by custom
				if (customIndex){
					this._indexByCustom[customIndex] = viewCid;
				}

				this._updateLength();
			},

			// Find a view by the model that was attached to
			// it. Uses the model's `cid` to find it.
			findByModel: function(model){
				return this.findByModelCid(model.cid);
			},

			// Find a view by the `cid` of the model that was attached to
			// it. Uses the model's `cid` to find the view `cid` and
			// retrieve the view using it.
			findByModelCid: function(modelCid){
				var viewCid = this._indexByModel[modelCid];
				return this.findByCid(viewCid);
			},

			// Find a view by a custom indexer.
			findByCustom: function(index){
				var viewCid = this._indexByCustom[index];
				return this.findByCid(viewCid);
			},

			// Find by index. This is not guaranteed to be a
			// stable index.
			findByIndex: function(index){
				return _.values(this._views)[index];
			},

			// retrieve a view by it's `cid` directly
			findByCid: function(cid){
				return this._views[cid];
			},

			findIndexByCid : function( cid ) {
				var index = -1;
				var view = _.find( this._views, function ( view ) {
					index++;
					if( view.model.cid == cid )
						return view;
				} );
				return ( view ) ? index : -1;
			},

			// Remove a view
			remove: function(view){
				var viewCid = view.cid;

				// delete model index
				if (view.model){
					delete this._indexByModel[view.model.cid];
				}

				// delete custom index
				_.any(this._indexByCustom, function(cid, key) {
					if (cid === viewCid) {
						delete this._indexByCustom[key];
						return true;
					}
				}, this);

				// remove the view from the container
				delete this._views[viewCid];

				// update the length
				this._updateLength();
			},

			// Call a method on every view in the container,
			// passing parameters to the call method one at a
			// time, like `function.call`.
			call: function(method){
				this.apply(method, _.tail(arguments));
			},

			// Apply a method on every view in the container,
			// passing parameters to the call method one at a
			// time, like `function.apply`.
			apply: function(method, args){
				_.each(this._views, function(view){
					if (_.isFunction(view[method])){
						view[method].apply(view, args || []);
					}
				});
			},

			// Update the `.length` attribute on this container
			_updateLength: function(){
				this.length = _.size(this._views);
			}
		});

		// Borrowing this code from Backbone.Collection:
		// http://backbonejs.org/docs/backbone.html#section-106
		//
		// Mix in methods from Underscore, for iteration, and other
		// collection related features.
		var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
			       'select', 'reject', 'every', 'all', 'some', 'any', 'include',
			       'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
			       'last', 'without', 'isEmpty', 'pluck'];

		_.each(methods, function(method) {
			Container.prototype[method] = function() {
				var views = _.values(this._views);
				var args = [views].concat(_.toArray(arguments));
				return _[method].apply(_, args);
			};
		});

		// return the public API
		return Container;
	})(Backbone, _);

	return Backbone.CollectionView;
} ) );

//
// backbone.trackit - 0.1.0
// The MIT License
// Copyright (c) 2013 The New York Times, CMS Group, Matthew DeLambo <delambo@gmail.com>
//
(function() {

	// Unsaved Record Keeping
	// ----------------------

	// Collection of all models in an app that have unsaved changes.
	var unsavedModels = [];

	// If the given model has unsaved changes then add it to
	// the `unsavedModels` collection, otherwise remove it.
	var updateUnsavedModels = function(model) {
		if (!_.isEmpty(model._unsavedChanges)) {
			if (!_.findWhere(unsavedModels, {cid:model.cid})) unsavedModels.push(model);
		} else {
			unsavedModels = _.filter(unsavedModels, function(m) { return model.cid != m.cid; });
		}
	};

	// Unload Handlers
	// ---------------

	// Helper which returns a prompt message for an unload handler.
	// Uses the given function name (one of the callback names
	// from the `model.unsaved` configuration hash) to evaluate
	// whether a prompt is needed/returned.
	var getPrompt = function(fnName) {
		var prompt, args = _.rest(arguments);
		// Evaluate and return a boolean result. The given `fn` may be a
		// boolean value, a function, or the name of a function on the model.
		var evaluateModelFn = function(model, fn) {
			if (_.isBoolean(fn)) return fn;
			return (_.isString(fn) ? model[fn] : fn).apply(model, args);
		};
		_.each(unsavedModels, function(model) {
			if (!prompt && evaluateModelFn(model, model._unsavedConfig[fnName]))
				prompt = model._unsavedConfig.prompt;
		});
		return prompt;
	};

	// Wrap Backbone.History.navigate so that in-app routing
	// (`router.navigate('/path')`) can be intercepted with a
	// confirmation if there are any unsaved models.
	Backbone.History.prototype.navigate = _.wrap(Backbone.History.prototype.navigate, function(oldNav, fragment, options) {
		var prompt = getPrompt('unloadRouterPrompt', fragment, options);
		if (prompt) {
			if (confirm(prompt + ' \n\nAre you sure you want to leave this page?')) {
				oldNav.call(this, fragment, options);
			}
		} else {
			oldNav.call(this, fragment, options);
		}
	});

	// Create a browser unload handler which is triggered
	// on the refresh, back, or forward button.
	window.onbeforeunload = function(e) {
		return getPrompt('unloadWindowPrompt', e);
	};

	// Backbone.Model API
	// ------------------

	_.extend(Backbone.Model.prototype, {

		unsaved: {},
		_trackingChanges: false,
		_originalAttrs: {},
		_unsavedChanges: {},

		// Opt in to tracking attribute changes
		// between saves.
		startTracking: function() {
			this._unsavedConfig = _.extend({}, {
				prompt: 'You have unsaved changes!',
				unloadRouterPrompt: false,
				unloadWindowPrompt: false
			}, this.unsaved || {});
			this._trackingChanges = true;
			this._resetTracking();
			this._triggerUnsavedChanges();
			return this;
		},

		// Resets the default tracking values
		// and stops tracking attribute changes.
		stopTracking: function() {
			this._trackingChanges = false;
			this._originalAttrs = {};
			this._unsavedChanges = {};
			this._triggerUnsavedChanges();
			return this;
		},

		// Gets rid of accrued changes and
		// resets state.
		restartTracking: function() {
			this._resetTracking();
			this._triggerUnsavedChanges();
			return this;
		},

		// Restores this model's attributes to
		// their original values since tracking
		// started, the last save, or last restart.
		resetAttributes: function() {
			if (!this._trackingChanges) return;
			this.attributes = this._originalAttrs;
			this._resetTracking();
			this._triggerUnsavedChanges();
			return this;
		},

		// Symmetric to Backbone's `model.changedAttributes()`,
		// except that this returns a hash of the model's attributes that
		// have changed since the last save, or `false` if there are none.
		// Like `changedAttributes`, an external attributes hash can be
		// passed in, returning the attributes in that hash which differ
		// from the model.
		unsavedAttributes: function(attrs) {
			if (!attrs) return _.isEmpty(this._unsavedChanges) ? false : _.clone(this._unsavedChanges);
			var val, changed = false, old = this._unsavedChanges;
			for (var attr in attrs) {
				if (_.isEqual(old[attr], (val = attrs[attr]))) continue;
				(changed || (changed = {}))[attr] = val;
			}
			return changed;
		},

		_resetTracking: function() {
			this._originalAttrs = _.clone(this.attributes);
			this._unsavedChanges = {};
		},

		// Trigger an `unsavedChanges` event on this model,
		// supplying the result of whether there are unsaved
		// changes and a changed attributes hash.
		_triggerUnsavedChanges: function() {
			this.trigger('unsavedChanges', !_.isEmpty(this._unsavedChanges), _.clone(this._unsavedChanges));
			if (this.unsaved) updateUnsavedModels(this);
		}
	});

	// Wrap `model.set()` and update the internal
	// unsaved changes record keeping.
	Backbone.Model.prototype.set = _.wrap(Backbone.Model.prototype.set, function(oldSet, key, val, options) {
		var attrs, ret;
		if (key == null) return this;
		// Handle both `"key", value` and `{key: value}` -style arguments.
		if (typeof key === 'object') {
			attrs = key;
			options = val;
		} else {
			(attrs = {})[key] = val;
		}
		options || (options = {});

		// Delegate to Backbone's set.
		ret = oldSet.call(this, attrs, options);

		if (this._trackingChanges && !options.silent) {
			_.each(attrs, _.bind(function(val, key) {
				if (_.isEqual(this._originalAttrs[key], val))
					delete this._unsavedChanges[key];
				else
					this._unsavedChanges[key] = val;
			}, this));
			this._triggerUnsavedChanges();
		}
		return ret;
	});

	// Intercept `model.save()` and reset tracking/unsaved
	// changes if it was successful.
	Backbone.sync = _.wrap(Backbone.sync, function(oldSync, method, model, options) {
		options || (options = {});

		if (method == 'update') {
			options.success = _.wrap(options.success, _.bind(function(oldSuccess, data, textStatus, jqXHR) {
				var ret;
				if (oldSuccess) ret = oldSuccess.call(this, data, textStatus, jqXHR);
				if (model._trackingChanges) {
					model._resetTracking();
					model._triggerUnsavedChanges();
				}
				return ret;
			}, this));
		}
		return oldSync(method, model, options);
	});

})();
define("vendor/backbone.trackit", function(){});

/**
 * Image object model for use in various models for the 'image' attribute
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Models/Image',[], function() {

	return Backbone.Model.extend( {

		defaults: {
			enabled: 'no',
			id: '',
			size: 'full',
			src: '',
		},

		initialize: function() {
			this.startTracking();
		},

	} );
} );

/**
 * Model relationships mixin
 *
 * @since    3.16.0
 * @version  3.16.11
 */
define( 'Models/_Relationships',[], function() {

	return {

		/**
		 * Default relationship settings object
		 *
		 * @type  {Object}
		 */
		relationship_defaults: {
			parent: {},
			children: {},
		},

		/**
		 * Relationship settings object
		 * Should be overridden in the model
		 *
		 * @type  {Object}
		 */
		relationships: {},

		/**
		 * Initialize all parent and child relationships
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		init_relationships: function( options ) {

			var rels = this.get_relationships();

			// initialize parent relationships
			// useful when adding a model to ensure parent is initialized
			if ( rels.parent && options && options.parent ) {
				this.set_parent( options.parent );
			}

			// initialize all children relationships
			_.each( rels.children, function( child_data, child_key ) {

				if ( ! child_data.conditional || true === child_data.conditional( this ) ) {

					var child_val = this.get( child_key ),
						child;

					if ( child_data.lookup ) {
						child = child_data.lookup( child_val );
					} else if ( 'model' === child_data.type ) {
						child = window.llms_builder.construct.get_model( child_data.class, child_val );
					} else if ( 'collection' === child_data.type ) {
						child = window.llms_builder.construct.get_collection( child_data.class, child_val );
					}

					this.set( child_key, child );

					// if the child defines a parent, save a reference to the parent on the child
					if ( 'model' === child_data.type ) {
						this._maybe_set_parent_reference( child );

						// save directly to each model in the collection
					} else if ( 'collection' === child_data.type ) {

						child.parent = this;
						child.each( function( child_model ) {

							this._maybe_set_parent_reference( child_model );

						}, this );

					}

				}

			}, this );

		},

		/**
		 * Retrieve the property names for all children of the model
		 *
		 * @return   array
		 * @since    3.16.11
		 * @version  3.16.11
		 */
		get_child_props: function() {

			var props = [];

			_.each( this.get_relationships().children, function( data, key ) {

				if ( ! data.conditional || true === data.conditional( this ) ) {
					props.push( key );
				}

			}, this );

			return props;

		},

		/**
		 * Retrieve the model's parent (if set)
		 *
		 * @return   obj|false
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_parent: function() {

			var rels = this.get_relationships();

			if ( rels.parent ) {
				return rels.parent.reference;
			}

			return false;

		},

		/**
		 * Retrieve relationships for the model
		 * Extends with defaults
		 *
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_relationships: function() {

			return $.extend( true, this.relationships, this.relationship_defaults );

		},

		/**
		 * Set the parent reference for the given model
		 *
		 * @param    obj   obj   parent model obj
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		set_parent: function( obj ) {
			this.relationships.parent.reference = obj;
		},

		/**
		 * Set up the parent relationships for qualifying children during relationship initialization
		 *
		 * @param    obj   model  child model
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_maybe_set_parent_reference: function( model ) {

			if ( ! model || ! model.get_relationships ) {
				return;
			}
			var rels = model.get_relationships();
			if ( rels.parent && rels.parent.model === this.get( 'type' ) ) {
				model.set_parent( this );
			}

		},

	};

} );

/**
 * Quiz Question Choice
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Models/QuestionChoice',[ 'Models/Image', 'Models/_Relationships' ], function( Image, Relationships ) {

	return Backbone.Model.extend( _.defaults( {

		/**
		 * Model relationships
		 *
		 * @type  {Object}
		 */
		relationships: {
			parent: {
				model: 'llms_question',
				type: 'model',
			},
			children: {
				choice: {
					conditional: function( model ) {
						return ( 'image' === model.get( 'choice_type' ) );
					},
					class: 'Image',
					model: 'image',
					type: 'model',
				},
			},
		},

		/**
		 * Model defaults
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		defaults: function() {
			return {
				id: _.uniqueId( 'temp_' ),
				choice: '',
				choice_type: 'text',
				correct: false,
				marker: 'A',
				question_id: '',
				type: 'choice',
			}
		},

		/**
		 * Initializer
		 *
		 * @param    obj   data     object of model attributes
		 * @param    obj   options  additional options
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function( data, options ) {

			this.startTracking();
			this.init_relationships( options );

		},

		/**
		 * Retrieve the choice's parent question
		 *
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_parent: function() {
			return this.collection.parent;
		},

		/**
		 * Retrieve the ID used when trashing the model
		 *
		 * @return   string
		 * @since    3.17.1
		 * @version  3.17.1
		 */
		get_trash_id: function() {
			return this.get( 'question_id' ) + ':' + this.get( 'id' );
		},

		/**
		 * Determine if "selection" is enabled for the question type
		 * Choice type questions are selectable by reorder type questions are not but still use choices
		 *
		 * @return   {Boolean}
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		is_selectable: function() {
			return this.get_parent().get( 'question_type' ).get_choice_selectable();
		},

	}, Relationships ) );

} );

/**
 * Question Choice Collection
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Collections/QuestionChoices',[ 'Models/QuestionChoice' ], function( model ) {

	return Backbone.Collection.extend( {

		/**
		 * Model for collection items
		 *
		 * @type  obj
		 */
		model: model,

		initialize: function() {

			// reorder called by QuestionList view when sortable drops occur
			this.on( 'reorder', this.update_order );

			// when a choice is added or removed, update order
			this.on( 'add', this.update_order );
			this.on( 'remove', this.update_order );

			// when a choice is added or remove, ensure min/max correct answers exist
			this.on( 'add', this.update_correct );
			this.on( 'remove', this.update_correct );

			// when a choice is toggled, ensure min/max correct exist
			this.on( 'correct-update', this.update_correct );

		},

		/**
		 * Retrieve the number of correct choices in the collection
		 *
		 * @return   int
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		count_correct: function() {

			return _.size( this.get_correct() );

		},

		/**
		 * Retrieve the collection reduced to only correct choices
		 *
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_correct: function() {
			return this.filter( function( choice ) {
				return choice.get( 'correct' );
			} );
		},

		/**
		 * Ensure min/max correct choices exist in the collection based on the question's settings
		 *
		 * @param    obj      choice  model of the choice that was toggled
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_correct: function( choice ) {

			if ( ! this.parent.get( 'question_type' ).get_choice_selectable() ) {
				return;
			}

			var siblings = this.without( choice ), // exclude the toggled choice from loops
				question = this.parent;

			// if multiple choices aren't enabled turn all other choices to incorrect
			if ( 'no' === question.get( 'multi_choices' ) ) {
				_.each( siblings, function( model ) {
					model.set( 'correct', false );
				} );
			}

			// if we don't have a single correct answer & the question has points, set one
			// allows users to create quizzes / questions with no points and therefore no correct answers are allowed
			if ( 0 === this.count_correct() && question.get( 'points' ) > 0 ) {
				var models = 1 === this.size() ? this.models : siblings;
				_.first( models ).set( 'correct', true );
			}

		},

		/**
		 * Update the marker attr of each choice in the list to reflect the order of the collection
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_order: function() {

			var self     = this,
				question = this.parent;

			this.each( function( choice ) {
				choice.set( 'marker', question.get( 'question_type' ).get_choice_markers()[ self.indexOf( choice ) ] );
			} );

		},

	} );

} );

/**
 * Quiz Question Type
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Models/QuestionType',[], function() {

	return Backbone.Model.extend( {

		/**
		 * Get model default attributes
		 *
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		defaults: function() {
			return {
				choices: false,
				clarifications: true,
				default_choices: [],
				description: true,
				icon: 'question',
				id: 'generic',
				image: true,
				keywords: [],
				name: 'Generic',
				placeholder: '',
				points: true,
				video: true,
			}
		},

		/**
		 * Retrieve an array of keywords for the question type
		 * Used for filtering questions by search term in the quiz builder
		 *
		 * @return   array
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_keywords: function() {

			var name  = this.get( 'name' ),
				words = [ name ];

			return words.concat( this.get( 'keywords' ) ).concat( name.split( ' ' ) );

		},

		/**
		 * Get marker array for the question choices
		 *
		 * @return   array
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_choice_markers: function() {

			return this._get_choice_option( 'markers' );

		},

		/**
		 * Determine if the question's choices are selectable
		 *
		 * @return   bool
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_choice_selectable: function() {

			return this._get_choice_option( 'selectable' );

		},

		/**
		 * Get the choice type (text,image)
		 *
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_choice_type: function() {

			return this._get_choice_option( 'type' );

		},

		/**
		 * Retrieve defined min. choices
		 *
		 * @return   int
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_min_choices: function() {

			return this._get_choice_option( 'min' );

		},

		/**
		 * Get type-defined max choices
		 *
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_max_choices: function() {

			return this._get_choice_option( 'max' );

		},

		/**
		 * Determine if multi-choice selection is enabled
		 *
		 * @return   bool
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_multi_choices: function() {

			var choices = this.get( 'choices' );

			if ( ! choices  ) {
				return false;
			}

			return this._get_choice_option( 'multi' );

		},

		/**
		 * Retrieve data from the type's "choices" attribute
		 * Allows quick handling of types with no choice definitions w/o additional checks
		 *
		 * @param    string   option  name of the choice option to retrieve
		 * @return   mixed
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_get_choice_option: function( option ) {

			var choices = this.get( 'choices' );

			if ( ! choices || ! choices[ option ] ) {
				return false;
			}

			return choices[ option ];

		},

	} );

} );

/**
 * Utility functions for Models
 *
 * @since    3.16.0
 * @version  3.17.1
 */
define( 'Models/_Utilities',[], function() {

	return {

		fields: [],

		/**
		 * Retrieve the edit post link for the current model
		 *
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_edit_post_link: function() {

			if ( this.has_temp_id() ) {
				return '';
			}

			return window.llms_builder.admin_url + 'post.php?post=' + this.get( 'id' ) + '&action=edit';

		},

		/**
		 * Retrieve schema fields defined for the model
		 *
		 * @return   object
		 * @since    3.17.0
		 * @version  3.17.1
		 */
		get_settings_fields: function() {

			var schema = this.schema || {};
			return window.llms_builder.schemas.get( schema, this.get( 'type' ).replace( 'llms_', '' ), this );

		},

		/**
		 * Determine if the model has a temporary ID
		 *
		 * @return   {Boolean}
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		has_temp_id: function() {

			return ( ! _.isNumber( this.get( 'id' ) ) && 0 === this.get( 'id' ).indexOf( 'temp_' ) );

		},

		/**
		 * Initializes 3rd party custom schema (field) data for a model
		 *
		 * @return   void
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		init_custom_schema: function() {

			var groups = _.filter( this.get_settings_fields(), function( group ) {
				return ( group.custom );
			} );

			_.each( groups, function( group ) {
				_.each( _.flatten( group.fields ), function( field ) {

					var keys    = [ field.attribute ],
						customs = this.get( 'custom' );

					if ( field.switch_attribute ) {
						keys.push( field.switch_attribute );
					}

					_.each( keys, function( key ) {
						var attr = field.attribute_prefix ? field.attribute_prefix + key : key;
						if ( customs && customs[ attr ] ) {
							this.set( key, customs[ attr ][0] );
						}
					}, this );

				}, this );
			}, this );

		},

	};

} );

/**
 * Quiz Question
 *
 * @since    3.16.0
 * @version  3.27.0
 */
define( 'Models/Question',[
		'Models/Image',
		'Collections/Questions',
		'Collections/QuestionChoices',
		'Models/QuestionType',
		'Models/_Relationships',
		'Models/_Utilities'
	], function(
		Image,
		Questions,
		QuestionChoices,
		QuestionType,
		Relationships,
		Utilities
	) {

		return Backbone.Model.extend( _.defaults( {

			/**
			 * Model relationships
			 *
			 * @type  {Object}
			 */
			relationships: {
				parent: {
					model: 'llms_quiz',
					type: 'model',
				},
				children: {
					choices: {
						class: 'QuestionChoices',
						model: 'choice',
						type: 'collection',
					},
					image: {
						class: 'Image',
						model: 'image',
						type: 'model',
					},
					questions: {
						class: 'Questions',
						conditional: function( model ) {
							var type = model.get( 'question_type' ),
							type_id  = _.isString( type ) ? type : type.get( 'id' );
							return ( 'group' === type_id );
						},
						model: 'llms_question',
						type: 'collection',
					},
					question_type: {
						class: 'QuestionType',
						lookup: function( val ) {
							if ( _.isString( val ) ) {
								return window.llms_builder.questions.get( val );
							}
							return val;
						},
						model: 'question_type',
						type: 'model',
					},
				}
			},

			/**
			 * Model defaults
			 *
			 * @return   obj
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			defaults: function() {
				return {
					id: _.uniqueId( 'temp_' ),
					choices: [],
					content: '',
					description_enabled: 'no',
					image: {},
					multi_choices: 'no',
					menu_order: 1,
					points: 1,
					question_type: 'generic',
					questions: [], // for question groups
					parent_id: '',
					title: '',
					type: 'llms_question',
					video_enabled: 'no',
					video_src: '',

					_expanded: false,
				}
			},

			/**
			 * Initializer
			 *
			 * @param    obj   data     object of data for the model
			 * @param    obj   options  additional options
			 * @return   void
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			initialize: function( data, options ) {

				var self = this;

				this.startTracking();
				this.init_relationships( options );

				if ( false !== this.get( 'question_type' ).choices ) {

					this._ensure_min_choices();

					// when a choice is removed, maybe add back some defaults so we always have the minimum
					this.listenTo( this.get( 'choices' ), 'remove', function() {
						// new items are added at index 0 when there's only 1 item in the collection, not sure why exactly...
						setTimeout( function() {
							self._ensure_min_choices();
						}, 0 );
					} );

				}

				// ensure question types that don't support points don't record default 1 point in database
				if ( ! this.get( 'question_type' ).get( 'points' ) ) {
					this.set( 'points', 0 );
				}

				_.delay( function( self ) {
					self.on( 'change:points', self.get_parent().update_points, self.get_parent() );
				}, 1, this );

			},

			/**
			 * Add a new question choice
			 *
			 * @param    obj   data     object of choice data
			 * @param    obj   options  additional options
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			add_choice: function( data, options ) {

				var max = this.get( 'question_type' ).get_max_choices();
				if ( this.get( 'choices' ).size() >= max ) {
					return;
				}

				data    = data || {};
				options = options || {};

				data.choice_type = this.get( 'question_type' ).get_choice_type();
				data.question_id = this.get( 'id' );
				options.parent   = this;

				var choice = this.get( 'choices' ).add( data, options );

				Backbone.pubSub.trigger( 'question-add-choice', choice, this );

			},

			/**
			 * Collapse question_type attribute during full syncs to save to database
			 * Not needed because question types cannot be adjusted after question creation
			 * Called from sync controller
			 *
			 * @param    obj      atts       flat object of attributes to be saved to db
			 * @param    string   sync_type  full or partial
			 *                                 full indicates a force resync or that the model isn't persisted yet
			 * @return   obj
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			before_save: function( atts, sync_type  ) {
				if ( 'full' === sync_type ) {
					atts.question_type = this.get( 'question_type' ).get( 'id' );
				}
				return atts;
			},

			/**
			 * Retrieve the model's parent (if set)
			 *
			 * @return   obj|false
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_parent: function() {

				var rels = this.get_relationships();

				if ( rels.parent ) {
					if ( this.collection && this.collection.parent ) {
						return this.collection.parent;
					} else if ( rels.parent.reference ) {
						return rels.parent.reference;
					}
				}

				return false;

			},

			/**
			 * Retrieve the translated post type name for the model's type
			 *
			 * @param    bool     plural  if true, returns the plural, otherwise returns singular
			 * @return   string
			 * @since    3.27.0
			 * @version  3.27.0
			 */
			get_l10n_type: function( plural ) {

				if ( plural ) {
					return LLMS.l10n.translate( 'questions' );
				}

				return LLMS.l10n.translate( 'question' );
			},

			/**
			 * Gets the index of the question within it's parent
			 * Question numbers skip content elements
			 * & content elements skip questions
			 *
			 * @return   int
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_type_index: function() {

				// current models type, used to check the predicate in the filter function below
				var curr_type = this.get( 'question_type' ).get( 'id' ),
				questions;

				questions = this.collection.filter( function( question ) {

					var type = question.get( 'question_type' ).get( 'id' );

					// if current model is not content, return all non-content questions
					if ( curr_type !== 'content' ) {
						return ( 'content' !== type );
					}

					// current model is content, return only content questions
					return 'content' === type;

				} );

				return questions.indexOf( this );

			},

			/**
			 * Gets iterator for the given type
			 * Questions use numbers and content uses alphabet
			 *
			 * @return   mixed
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_type_iterator: function() {

				var index = this.get_type_index();

				if ( -1 === index ) {
					return '';
				}

				if ( 'content' === this.get( 'question_type' ).get( 'id' ) ) {
					var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split( '' );
					return alphabet[ index ];
				}

				return index + 1;

			},

			get_qid: function() {

				var parent = this.get_parent_question(),
				prefix     = '';

				if ( parent ) {

					prefix = parent.get_qid() + '.';

				}

				// return short_id + this.get_type_iterator();
				return prefix + this.get_type_iterator();

			},

			/**
			 * Retrieve the parent question (if the question is in a question group)
			 *
			 * @return   obj|false
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_parent_question: function() {

				if ( this.is_in_group() ) {

					return this.collection.parent;

				}

				return false;

			},

			/**
			 * Retrieve the parent quiz
			 *
			 * @return   obj
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_parent_quiz: function() {
				return this.get_parent();
			},

			/**
			 * Points getter
			 * ensures that 0 is always returned if the question type doesn't support points
			 *
			 * @return   int
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_points: function() {

				if ( ! this.get( 'question_type' ).get( 'points' ) ) {
					return 0;
				}

				return this.get( 'points' );

			},

			/**
			 * Retrieve the questions percentage value within the quiz
			 *
			 * @return   string
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			get_points_percentage: function() {

				var total = this.get_parent().get( '_points' ),
				points    = this.get( 'points' );

				if ( 0 === total ) {
					return '0%';
				}

				return ( ( points / total ) * 100 ).toFixed( 2 ) + '%';

			},

			/**
			 * Determine if the question belongs to a question group
			 *
			 * @return   {Boolean}
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			is_in_group: function() {

				return ( 'question' === this.collection.parent.get( 'type' ) );

			},

			_ensure_min_choices: function() {

				var choices = this.get( 'choices' );
				while ( choices.size() < this.get( 'question_type' ).get_min_choices() ) {
					this.add_choice();
				}

			},

		}, Relationships, Utilities ) );

} );

/**
 * Questions Collection
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Collections/Questions',[ 'Models/Question' ], function( model ) {

	return Backbone.Collection.extend( {

		/**
		 * Model for collection items
		 *
		 * @type  obj
		 */
		model: model,

		/**
		 * Initialize
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function() {

			// reorder called by QuestionList view when sortable drops occur
			this.on( 'reorder', this.update_order );

			// when a question is added or removed, update order
			this.on( 'add', this.update_order );
			this.on( 'remove', this.update_order );

			this.on( 'add', this.update_parent );

		},

		/**
		 * Update the order attr of each question in the list to reflect the order of the collection
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_order: function() {

			var self = this;

			this.each( function( question ) {

				question.set( 'menu_order', self.indexOf( question ) + 1 );

			} );

		},

		/**
		 * When adding a question to a question list, update the question's parent
		 * Will ensure that questions moved into and out of groups always have the correct parent_id
		 *
		 * @param    obj   model  instance of the question model
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_parent: function( model ) {

			model.set( 'parent_id', this.parent.get( 'id' ) );

		},

	} );

} );

/**
 * Quiz Schema
 *
 * @since    3.17.6
 * @version  3.24.0
 */
define( 'Schemas/Quiz',[], function() {

	return window.llms.hooks.applyFilters( 'llms_define_quiz_schema', {

		default: {
			title: LLMS.l10n.translate( 'General Settings' ),
			toggleable: true,
			fields: [
				[
					{
						attribute: 'permalink',
						id: 'permalink',
						type: 'permalink',
			},
				], [
					{
						attribute: 'content',
						id: 'description',
						label: LLMS.l10n.translate( 'Description' ),
						type: 'editor',
			},
				], [
					{
						attribute: 'passing_percent',
						id: 'passing-percent',
						label: LLMS.l10n.translate( 'Passing Percentage' ),
						min: 0,
						max: 100,
						tip: LLMS.l10n.translate( 'Minimum percentage of total points required to pass the quiz' ),
						type: 'number',
			},
					{
						attribute: 'allowed_attempts',
						id: 'allowed-attempts',
						label: LLMS.l10n.translate( 'Limit Attempts' ),
						switch_attribute: 'limit_attempts',
						tip: LLMS.l10n.translate( 'Limit the maximum number of times a student can take this quiz' ),
						type: 'switch-number',
			},
					{
						attribute: 'time_limit',
						id: 'time-limit',
						label: LLMS.l10n.translate( 'Time Limit' ),
						min: 1,
						max: 360,
						switch_attribute: 'limit_time',
						tip: LLMS.l10n.translate( 'Enforce a maximum number of minutes a student can spend on each attempt' ),
						type: 'switch-number',
			},
				], [
					{
						attribute: 'show_correct_answer',
						id: 'show-correct-answer',
						label: LLMS.l10n.translate( 'Show Correct Answers' ),
						tip: LLMS.l10n.translate( 'When enabled, students will be shown the correct answer to any question they answered incorrectly.' ),
						type: 'switch',
			},
					{
						attribute: 'random_questions',
						id: 'random-questions',
						label: LLMS.l10n.translate( 'Randomize Question Order' ),
						tip: LLMS.l10n.translate( 'Display questions in a random order for each attempt. Content questions are locked into their defined positions.' ),
						type: 'switch',
			},
				],

			],
		},

	} );

} );

/**
 * Quiz Model
 * @since    3.16.0
 * @version  3.24.0
 */
define( 'Models/Quiz',[
		'Collections/Questions',
		'Models/Lesson',
		'Models/Question',
		'Models/_Relationships',
		'Models/_Utilities',
		'Schemas/Quiz',
	], function(
		Questions,
		Lesson,
		Question,
		Relationships,
		Utilities,
		QuizSchema
	) {

	return Backbone.Model.extend( _.defaults( {

		/**
		 * model relationships
		 * @type  {Object}
		 */
		relationships: {
			parent: {
				model: 'lesson',
				type: 'model',
			},
			children: {
				questions: {
					class: 'Questions',
					model: 'llms_question',
					type: 'collection',
				},
			}
		},

		/**
		 * Lesson Settings Schema
		 * @type  {Object}
		 */
		schema: QuizSchema,

		/**
		 * New lesson defaults
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.6
		 */
		defaults: function() {

			return {
				id: _.uniqueId( 'temp_' ),
				title: LLMS.l10n.translate( 'New Quiz' ),
				type: 'llms_quiz',
				lesson_id: '',

				status: 'draft',

				// editable fields
				content: '',
				allowed_attempts: 5,
				limit_attempts: 'no',
				limit_time: 'no',
				passing_percent: 65,
				name: '',
				random_answers: 'no',
				time_limit: 30,
				show_correct_answer: 'no',

				questions: [],

				// calculated
				_points: 0,

				// display
				permalink: '',
				_show_settings: false,
				_questions_loaded: false,
			};

		},

		/**
		 * Initializer
		 * @return   void
		 * @since    3.16.0
		 * @version  3.24.0
		 */
		initialize: function() {

			this.init_custom_schema();
			this.startTracking();
			this.init_relationships();

			this.listenTo( this.get( 'questions' ), 'add', this.update_points );
			this.listenTo( this.get( 'questions' ), 'remove', this.update_points );

			this.set( '_points', this.get_total_points() );

			// when a quiz is published, ensure the parent lesson is marked as "Enabled" for quizzing
			this.on( 'change:status', function() {
				if ( 'publish' === this.get( 'status' ) ) {
					this.get_parent().set( 'quiz_enabled', 'yes' );
				}
			} );

			window.llms.hooks.doAction( 'llms_quiz_model_init', this );

		},

		/**
		 * Add a new question to the quiz
		 * @param    obj   data   question data
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		add_question: function( data ) {

			data.parent_id = this.get( 'id' );
			var question = this.get( 'questions' ).add( data, {
				parent: this,
			} );
			Backbone.pubSub.trigger( 'quiz-add-question', question, this );

		},

		/**
		 * Retrieve the translated post type name for the model's type
		 * @param    bool     plural  if true, returns the plural, otherwise returns singular
		 * @return   string
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		get_l10n_type: function( plural ) {

			if ( plural ) {
				return LLMS.l10n.translate( 'quizzes' );
			}

			return LLMS.l10n.translate( 'quiz' );
		},

		/**
		 * Retrieve the quiz's total points
		 * @return   int
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_total_points: function() {

			var points = 0;

			this.get( 'questions' ).each( function( question ) {
				points += question.get_points();
			} );

			return points;

		},

		/**
		 * Lazy load questions via AJAX
		 * @param    {Function}  cb  callback function
		 * @return   void
		 * @since    3.19.2
		 * @version  3.19.2
		 */
		load_questions: function( cb ) {

			if ( this.get( '_questions_loaded' ) ) {

				cb();

			} else {

				var self = this;

				LLMS.Ajax.call( {
					data: {
						action: 'llms_builder',
						action_type: 'lazy_load',
						course_id: window.llms_builder.CourseModel.get( 'id' ),
						load_id: this.get( 'id' ),
					},
					error: function( xhr, status, error ) {

						console.log( xhr, status, error );
						window.llms_builder.debug.log( '==== start load_questions error ====', xhr, status, error, '==== finish load_questions error ====' );
						cb( true );

					},
					success: function( res ) {
						if ( res && res.questions ) {
							self.set( '_questions_loaded', true );
							if ( res.questions ) {
								_.each( res.questions, self.add_question, self );
							}
							cb();
						} else {
							cb( true );
						}
					}

				} );

			}


		},

		/**
		 * Update total number of points calculated property
		 * @return   int
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_points: function() {

			this.set( '_points', this.get_total_points() );

		},

	}, Relationships, Utilities ) );

} );

/**
 * Lesson Schemas
 *
 * @since    3.17.0
 * @version  3.25.4
 */
define( 'Schemas/Lesson',[], function() {

	return window.llms.hooks.applyFilters( 'llms_define_lesson_schema', {

		default: {
			title: LLMS.l10n.translate( 'General Settings' ),
			toggleable: true,
			fields: [
				[
					{
						attribute: 'permalink',
						id: 'permalink',
						type: 'permalink',
			},
				], [
					{
						attribute: 'video_embed',
						id: 'video-embed',
						label: LLMS.l10n.translate( 'Video Embed URL' ),
						type: 'video_embed',
			},
					{
						attribute: 'audio_embed',
						id: 'audio-embed',
						label: LLMS.l10n.translate( 'Audio Embed URL' ),
						type: 'audio_embed',
			},
				], [
					{
						attribute: 'free_lesson',
						id: 'free-lesson',
						label: LLMS.l10n.translate( 'Free Lesson' ),
						tip: LLMS.l10n.translate( "Free lessons can be accessed without enrollment." ),
						type: 'switch',
			},
					{
						attribute: 'require_passing_grade',
						id: 'require-passing-grade',
						label: LLMS.l10n.translate( 'Require Passing Grade on Quiz' ),
						tip: LLMS.l10n.translate( "When enabled, students must pass this lesson's quiz before the lesson can be completed." ),
						type: 'switch',
						condition: function() {
							return ( 'yes' === this.get( 'quiz_enabled' ) );
						},
			},
					{
						attribute: 'require_assignment_passing_grade',
						id: 'require-assignment-passing-grade',
						label: LLMS.l10n.translate( 'Require Passing Grade on Assignment' ),
						tip: LLMS.l10n.translate( "When enabled, students must pass this lesson's assignment before the lesson can be completed." ),
						type: 'switch',
						condition: function() {
							return ( 'undefined' !== window.llms_builder.assignments && 'yes' === this.get( 'assignment_enabled' ) );
						},
			},
					{
						attribute: 'points',
						id: 'points',
						label: LLMS.l10n.translate( 'Lesson Weight' ),
						label_after: LLMS.l10n.translate( 'POINTS' ),
						min: 0,
						max: 99,
						tip: LLMS.l10n.translate( 'Determines the weight of the lesson when calculating the overall grade of the course.' ),
						tip_position: 'top-left',
						type: 'number',
						condition: function() {
							return ( ( 'yes' === this.get( 'quiz_enabled' ) ) || ( 'undefined' !== window.llms_builder.assignments && 'yes' === this.get( 'assignment_enabled' ) ) );
						},
			},
				], [
					{
						attribute: 'prerequisite',
						condition: function() {
							return ( false === this.is_first_in_course() );
						},
						id: 'prerequisite',
						label: LLMS.l10n.translate( 'Prerequisite' ),
						switch_attribute: 'has_prerequisite',
						type: 'switch-select',
						options: function() {
							return this.get_available_prereq_options();
						},
			},
				], [
					{
						attribute: 'drip_method',
						id: 'drip-method',
						label: LLMS.l10n.translate( 'Drip Method' ),
						switch_attribute: 'drip_method',
						type: 'select',
						options: function() {

							var options = [
								{
									key: '',
									val: LLMS.l10n.translate( 'None' ),
							},
								{
									key: 'date',
									val: LLMS.l10n.translate( 'On a specific date' ),
							},
								{
									key: 'enrollment',
									val: LLMS.l10n.translate( '# of days after course enrollment' ),
							},
							];

							if ( this.get_course() && this.get_course().get( 'start_date' ) ) {
								options.push( {
									key: 'start',
									val: LLMS.l10n.translate( '# of days after course start date' ),
								} );
							}

							if ( 'yes' === this.get( 'has_prerequisite' ) ) {
								options.push( {
									key: 'prerequisite',
									val: LLMS.l10n.translate( '# of days after prerequisite lesson completion' ),
								} );
							}

							return options;

						},
			},
					{
						attribute: 'days_before_available',
						condition: function() {
							return ( -1 !== [ 'enrollment', 'start', 'prerequisite' ].indexOf( this.get( 'drip_method' ) ) );
						},
						id: 'days-before-available',
						label: LLMS.l10n.translate( '# of days' ),
						min: 0,
						type: 'number',
			},
					{
						attribute: 'date_available',
						date_format: 'Y-m-d',
						condition: function() {
							return ( 'date' === this.get( 'drip_method' ) );
						},
						id: 'date-available',
						label: LLMS.l10n.translate( 'Date' ),
						timepicker: 'false',
						type: 'datepicker',
			},
					{
						attribute: 'time_available',
						condition: function() {
							return ( 'date' === this.get( 'drip_method' ) );
						},
						datepicker: 'false',
						date_format: 'h:i A',
						id: 'time-available',
						label: LLMS.l10n.translate( 'Time' ),
						type: 'datepicker',
			},
				],
			],
		},

	} );

} );

/**
 * Lesson Model
 *
 * @since 3.13.0
 * @version 4.20.0
 */
define( 'Models/Lesson',[ 'Models/Quiz', 'Models/_Relationships', 'Models/_Utilities', 'Schemas/Lesson' ], function( Quiz, Relationships, Utilities, LessonSchema ) {

	return Backbone.Model.extend( _.defaults( {

		/**
		 * Model relationships
		 *
		 * @type {Object}
		 */
		relationships: {
			parents: {
				model: 'section',
				type: 'model',
			},
			children: {
				quiz: {
					class: 'Quiz',
					conditional: function( model ) {
						// if quiz is enabled OR not enabled but we have some quiz data as an obj
						return ( 'yes' === model.get( 'quiz_enabled' ) || ! _.isEmpty( model.get( 'quiz' ) ) );
					},
					model: 'llms_quiz',
					type: 'model',
				},
			},
		},

		/**
		 * Lesson Settings Schema
		 *
		 * @type {Object}
		 */
		schema: LessonSchema,

		/**
		 * New lesson defaults
		 *
		 * @since 3.13.0
		 * @since 3.24.0 Unknown.
		 *
		 * @return {Object} Default options associative array (js object).
		 */
		defaults: function() {
			return {
				id: _.uniqueId( 'temp_' ),
				title: LLMS.l10n.translate( 'New Lesson' ),
				type: 'lesson',
				order: this.collection ? this.collection.length + 1 : 1,
				parent_course: window.llms_builder.course.id,
				parent_section: '',

				// Urls.
				edit_url: '',
				view_url: '',

				// Editable fields.
				content: '',
				audio_embed: '',
				has_prerequisite: 'no',
				require_passing_grade: 'yes',
				require_assignment_passing_grade: 'yes',
				video_embed: '',
				free_lesson: '',
				points: 1,

				// Other fields.
				assignment: {}, // Assignment model/data.
				assignment_enabled: 'no',

				quiz: {}, // Quiz model/data.
				quiz_enabled: 'no',

				_forceSync: false,

			};
		},

		/**
		 * Initializer
		 *
		 * @since 3.16.0
		 * @since 3.17.0 Unknown
		 *
		 * @return {void}
		 */
		initialize: function() {

			this.init_custom_schema();
			this.startTracking();
			this.maybe_init_assignments();
			this.init_relationships();

			// If the lesson ID isn't set on a quiz, set it.
			var quiz = this.get( 'quiz' );
			if ( ! _.isEmpty( quiz ) && ! quiz.get( 'lesson_id' ) ) {
				quiz.set( 'lesson_id', this.get( 'id' ) );
			}

			window.llms.hooks.doAction( 'llms_lesson_model_init', this );

		},

		/**
		 * Retrieve a reference to the parent course of the lesson
		 *
		 * @since 3.16.0
		 * @since 4.14.0 Use Section.get_course() in favor of Section.get_parent().
		 *
		 * @return {Object} The parent course model of the lesson.
		 */
		get_course: function() {
			return this.get_parent().get_course();
		},

		/**
		 * Retrieve the translated post type name for the model's type
		 *
		 * @since  3.16.12
		 *
		 * @param bool plural If true, returns the plural, otherwise returns singular.
		 * @return string The translated post type name.
		 */
		get_l10n_type: function( plural ) {

			if ( plural ) {
				return LLMS.l10n.translate( 'lessons' );
			}

			return LLMS.l10n.translate( 'lesson' );
		},

		/**
		 * Override default get_parent to grab from collection if models parent isn't set
		 *
		 * @since 3.17.0
		 *
		 * @return {Object}|false The parent model or false if not available.
		 */
		get_parent: function() {

			var rels = this.get_relationships();
			if ( rels.parent && rels.parent.reference ) {
				return rels.parent.reference;
			} else if ( this.collection && this.collection.parent ) {
				return this.collection.parent;
			}
			return false;

		},

		/**
		 * Retrieve the questions percentage value within the quiz
		 *
		 * @since 3.24.0
		 *
		 * @return {String} Questions percentage value within the quiz.
		 */
		get_points_percentage: function() {

			var total  = this.get_course().get_total_points(),
				points = this.get( 'points' ) * 1;

			if ( ! _.isNumber( points ) ) {
				points = 0;
			}

			if ( 0 === total ) {
				return '0%';
			}

			return ( ( points / total ) * 100 ).toFixed( 2 ) + '%';

		},

		/**
		 * Retrieve an array of prerequisite options available for the current lesson
		 *
		 * @since 3.17.0
		 *
		 * @return {Object} Prerequisite options.
		 */
		get_available_prereq_options: function() {

			var parent_section_index    = this.get_parent().collection.indexOf( this.get_parent() ),
				lesson_index_in_section = this.collection.indexOf( this ),
				options                 = [];

			this.get_course().get( 'sections' ).each( function( section, curr_sec_index ) {
				if ( curr_sec_index <= parent_section_index ) {
					var group = {
							// Translators: %1$d = section order number, %2$s = section title.
						label: LLMS.l10n.replace( 'Section %1$d: %2$s', {
							'%1$d': section.get( 'order' ),
							'%2$s': section.get( 'title' )
						} ),
					options: [],
					};

					section.get( 'lessons' ).each( function( lesson, curr_les_index ) {
						if ( curr_sec_index !== parent_section_index || curr_les_index < lesson_index_in_section ) {
							// Translators: %1$d = lesson order number, %2$s = lesson title.
							group.options.push( {
								key: lesson.get( 'id' ),
								val: LLMS.l10n.replace( 'Lesson %1$d: %2$s', {
									'%1$d': lesson.get( 'order' ),
									'%2$s': lesson.get( 'title' )
								} ),
							} );
						}
					}, this );

					options.push( group );
				}
			}, this );

			return options;

		},

		/**
		 * Add a new quiz to the lesson
		 *
		 * @since 3.16.0
		 * @since 3.27.0 Unknown.
		 *
		 * @param {Object} data Object of quiz data used to construct a new quiz model.
		 * @return {Object} Model for the created quiz.
		 */
		add_quiz: function( data ) {

			data = data || {};

			data.lesson_id         = this.id;
			data._questions_loaded = true;

			if ( ! data.title ) {

				data.title = LLMS.l10n.replace( '%1$s Quiz', {
					'%1$s': this.get( 'title' ),
				} );

			}

			this.set( 'quiz', data );
			this.init_relationships();

			var quiz = this.get( 'quiz' );
			this.set( 'quiz_enabled', 'yes' );

			window.llms.hooks.doAction( 'llms_lesson_add_quiz', quiz, this );

			return quiz;

		},

		/**
		 * Determine if this is the first lesson
		 *
		 * @since 3.17.0
		 * @since 4.20.0 Use is_first_in_section() new method.
		 *
		 * @return {Boolean} Whether this is the first lesson of its course.
		 */
		is_first_in_course: function() {

			// If it's not the first item in the section it cant be the first lesson.
			if ( ! this.is_first_in_section() ) {
				return false;
			}

			// If it's not the first section it cant' be first lesson.
			var section = this.get_parent();
			if ( section.collection.indexOf( section ) ) {
				return false;
			}

			// It's first lesson in first section.
			return true;

		},

		/**
		 * Determine if this is the last lesson of the course
		 *
		 * @since 4.20.0
		 *
		 * @return {Boolean} Whether this is the last lesson of its course.
		 */
		 is_last_in_course: function() {

			// If it's not last item in the section it cant be the last lesson.
			if ( ! this.is_last_in_section() ) {
				return false;
			}

			// If it's not the last section it cant' be last lesson.
			var section = this.get_parent();
			if ( section.collection.indexOf( section ) < ( section.collection.size() - 1 ) ) {
				return false;
			}

			// It's last lesson in last section.
			return true;

		},

		/**
		 * Determine if this is the first lesson within its section
		 *
		 * @since 4.20.0
		 *
		 * @return {Boolean} Whether this is the first lesson of its section.
		 */
		is_first_in_section: function() {
			return 0 === this.collection.indexOf( this );
		},

		/**
		 * Determine if this is the last lesson within its section
		 *
		 * @since 4.20.0
		 *
		 * @return {Boolean} Whether this is the last lesson of its section.
		 */
		is_last_in_section: function() {
			return this.collection.indexOf( this ) === ( this.collection.size() - 1 );
		},

		/**
		 * Get prev lesson in a course
		 *
		 * @since 4.20.0
		 *
		 * @param {String} status Prev lesson post status. If not specified any status will be taken into account.
		 * @return {Object}|false Previous lesson model or `false` if no previous lesson could be found.
		 */
		get_prev: function( status ) {
			return this.get_sibling( 'prev', status );
		},

		/**
		 * Get next lesson in a course
		 *
		 * @since 4.20.0
		 *
		 * @param {String} status Next lesson post status. If not specified any status will be taken into account.
		 * @return {Object}|false Next lesson model or `false` if no next lesson could be found.
		 */
		get_next: function( status ) {
			return this.get_sibling( 'next', status );
		},

		/**
		 * Get a sibling lesson
		 *
		 * @param {String} direction Siblings direction [next|prev]. If not specified will fall back on 'prev'.
		 * @param {String} status    Sibling lesson post status. If not specified any status will be taken into account.
		 * @return {Object}|false Sibling lesson model, in the specified direction, or `false` if no sibling lesson could be found.
		 */
		get_sibling: function( direction, status ) {

			direction = 'next' === direction ? direction : 'prev';

			// Functions and vars to use when direction is 'prev' (default).
			var is_course_limit_reached_f               = 'is_first_in_course',
				is_section_limit_reached_f              = 'is_first_in_section',
				sibling_index_increment                 = -1,
				get_sibling_lesson_in_sibling_section_f = 'last';

			if ( 'next' === direction ) {
				is_course_limit_reached_f               = 'is_last_in_course';
				is_section_limit_reached_f              = 'is_last_in_section';
				sibling_index_increment                 = 1,
				get_sibling_lesson_in_sibling_section_f = 'first';
			}

			if ( this[ is_course_limit_reached_f ]() ) {
				return false;
			}

			var sibling_index  = this.collection.indexOf( this ) + sibling_index_increment,
				sibling_lesson = this.collection.at( sibling_index );

			if ( this[ 'next' === direction ? 'is_last_in_section' : 'is_first_in_section' ]() ) {
				var cur_section     = this.get_parent(),
					sibling_section = cur_section[ 'get_' + direction ]( false );

				// Skip sibling empty sections.
				while ( sibling_section && ! sibling_section.get( 'lessons' ).size() ) {
					sibling_section = sibling_section[ 'get_' + direction ]( false );
				}

				// Couldn't find any suitable lesson.
				if ( ! sibling_section || ! sibling_section.get( 'lessons' ).size() ) {
					return false;
				}

				sibling_lesson = sibling_section.get( 'lessons' )[ get_sibling_lesson_in_sibling_section_f ]();

			}

			// If we need a specific lesson status.
			if ( status && status !== sibling_lesson.get( 'status' ) ) {
				return sibling_lesson.get_sibling( direction, status );
			}

			return sibling_lesson;

		},

		/**
		 * Initialize lesson assignments *if* the assignments addon is available and enabled
		 *
		 * @since 3.17.0
		 *
		 * @return {Void}
		 */
		maybe_init_assignments: function() {

			if ( ! window.llms_builder.assignments ) {
				return;
			}

			this.relationships.children.assignment = {
				class: 'Assignment',
				conditional: function( model ) {
					// If assignment is enabled OR not enabled but we have some assignment data as an obj.
					return ( 'yes' === model.get( 'assignment_enabled' ) || ! _.isEmpty( model.get( 'assignment' ) ) );
				},
				model: 'llms_assignment',
				type: 'model',
			};

		},

	}, Relationships, Utilities ) );

} );

/**
 * Lessons Collection
 *
 * @since    3.13.0
 * @version  3.17.0
 */
define( 'Collections/Lessons',[ 'Models/Lesson' ], function( model ) {

	return Backbone.Collection.extend( {

		/**
		 * Model for collection items
		 *
		 * @type  obj
		 */
		model: model,

		/**
		 * Initializer
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.17.0
		 */
		initialize: function() {

			// reorder called by LessonList view when sortable drops occur
			this.on( 'reorder', this.on_reorder );

			// when a lesson is added or removed, update order
			this.on( 'add', this.on_reorder );
			this.on( 'remove', this.on_reorder );

		},

		/**
		 * On lesson reorder callback
		 *
		 * Update the order attr of each lesson to reflect the new lesson order
		 * Validate prerequisite (if set) and unset it if it's no longer a valid prereq
		 *
		 * @return   void
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		on_reorder: function() {
			this.update_order();
			this.validate_prereqs();
		},

		/**
		 * Update lesson order attribute of all lessons when lessons are reordered
		 *
		 * @return      void
		 * @since       3.16.0
		 * @version     3.17.0
		 */
		update_order: function() {

			this.each( function( lesson ) {
				lesson.set( 'order', this.indexOf( lesson ) + 1 );
			}, this );

		},

		/**
		 * Validate prerequisite (if set) and unset it if it's no longer a valid prereq
		 *
		 * @return   void
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		validate_prereqs: function() {

			this.each( function( lesson ) {

				// validate prereqs
				if ( 'yes' === lesson.get( 'has_prerequisite' ) ) {
					var valid = _.pluck( _.flatten( _.pluck( lesson.get_available_prereq_options(), 'options' ) ), 'key' );
					if ( -1 === valid.indexOf( lesson.get( 'prerequisite' ) * 1 ) ) {
						lesson.set( {
							prerequisite: 0,
							has_prerequisite: 'no',
						} );
					}
				}

			}, this );

		},

	} );

} );

/**
 * Quiz Question Type Collection
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Collections/QuestionTypes',[ 'Models/QuestionType' ], function( model ) {

	return Backbone.Collection.extend( {

		/**
		 * Model for collection items
		 *
		 * @type  obj
		 */
		model: model,

		/**
		 * Initializer
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function() {

			this.on( 'add', this.comparator );
			this.on( 'remove', this.comparator );

		},

		/**
		 * Comparator (sorts collection)
		 *
		 * @param    obj   model  QuestionType model
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		comparator: function( model ) {

			return model.get( 'group' ).order;

		},

	} );

} );

/**
 * Section Model
 *
 * @since 3.16.0
 * @version 4.20.0
 */
define( 'Models/Section',[ 'Collections/Lessons', 'Models/_Relationships' ], function( Lessons, Relationships ) {

	return Backbone.Model.extend( _.defaults( {

		relationships: {
			parent: {
				model: 'course',
				type: 'model',
			},
			children: {
				lessons: {
					class: 'Lessons',
					model: 'lesson',
					type: 'collection',
				},
			}
		},

		/**
		 * New section defaults
		 *
		 * @since 3.16.0
		 *
		 * @return {Object}
		 */
		defaults: function() {
			return {
				id: _.uniqueId( 'temp_' ),
				lessons: [],
				order: this.collection ? this.collection.length + 1 : 1,
				parent_course: window.llms_builder.course.id,
				title: LLMS.l10n.translate( 'New Section' ),
				type: 'section',

				_expanded: false,
				_selected: false,
			};
		},

		/**
		 * Initialize
		 *
		 * @since 3.16.0
		 *
		 * @return {void}
		 */
		initialize: function() {

			this.startTracking();
			this.init_relationships();

		},

		/**
		 * Add a lesson to the section
		 *
		 * @since 3.16.0
		 * @since 3.16.11 Unknown.
		 *
		 * @param {Object} data    Hash of lesson data (creates new lesson)
		 *                         or existing lesson as a Backbone.Model.
		 * @param {Object} options Hash of options.
		 * @return {Object} Backbone.Model of the new/updated lesson.
		 */
		add_lesson: function( data, options ) {

			data    = data || {};
			options = options || {};

			if ( data instanceof Backbone.Model ) {
				data.set( 'parent_section', this.get( 'id' ) );
				data.set_parent( this );
			} else {
				data.parent_section = this.get( 'id' );
			}

			return this.get( 'lessons' ).add( data, options );

		},

		/**
		 * Retrieve the translated post type name for the model's type
		 *
		 * @since 3.16.12
		 *
		 * @param {Boolean} plural If true, returns the plural, otherwise returns singular.
		 * @return {String}
		 */
		get_l10n_type: function( plural ) {

			if ( plural ) {
				return LLMS.l10n.translate( 'sections' );
			}

			return LLMS.l10n.translate( 'section' );
		},

		/**
		 * Get next section in the collection
		 *
		 * @since 3.16.11
		 *
		 * @param {boolean} circular If true handles the collection in a circle.
		 *                           If current is the last section, returns the first section.
		 * @return {Object}|false
		 */
		get_next: function( circular ) {
			return this._get_sibling( 'next', circular );
		},

		/**
		 * Retrieve a reference to the parent course of the section
		 *
		 * @since 4.14.0
		 *
		 * @return {Object}
		 */
		get_course: function() {

			// When working with an unsaved draft course the parent isn't properly set on the creation of a section.
			if ( ! this.get_parent() ) {
				this.set_parent( window.llms_builder.CourseModel );
			}

			return this.get_parent();

		},

		/**
		 * Get prev section in the collection
		 *
		 * @since 3.16.11
		 *
		 * @param {Boolean} circular If true handles the collection in a circle.
		 *                           If current is the first section, returns the last section.
		 * @return {Object}|false
		 */
		get_prev: function( circular ) {
			return this._get_sibling( 'prev', circular );
		},

		/**
		 * Get a sibling section
		 *
		 * @since 3.16.11
		 * @since 4.20.0 Fix case when the last section was returned when looking for the prev of the first section and not `circular`.
		 *
		 * @param {String}  direction Siblings direction [next|prev].
		 * @param {Boolean} circular  If true handles the collection in a circle.
		 *                            If current is the last section, returns the first section.
		 *                            If current is the first section, returns the last section.
		 * @return {Object}|false
		 */
		_get_sibling: function( direction, circular ) {

			circular = ( 'undefined' === circular ) ? true : circular;

			var max   = this.collection.size() - 1,
				index = this.collection.indexOf( this ),
				sibling_index;

			if ( 'next' === direction ) {
				sibling_index = index + 1;
			} else if ( 'prev' === direction ) {
				sibling_index = index - 1;
			}

			// Don't retrieve greater than max or less than min.
			if ( sibling_index <= max || sibling_index >= 0 ) {

				return this.collection.at( sibling_index );

			} else if ( circular ) {

				if ( 'next' === direction ) {
					return this.collection.first();
				} else if ( 'prev' === direction ) {
					return this.collection.last();
				}

			}

			return false;

		},

	}, Relationships ) );

} );

/**
 * Sections Collection
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Collections/Sections',[ 'Models/Section' ], function( model ) {

	return Backbone.Collection.extend( {

		/**
		 * Model for collection items
		 *
		 * @type  obj
		 */
		model: model,

		/**
		 * Initialize
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function() {

			var self = this;

			// reorder called by SectionList view when sortable drops occur
			this.on( 'reorder', this.update_order );

			// when a section is added or removed, update order
			this.on( 'add', this.update_order );
			this.on( 'remove', this.update_order );

		},

		/**
		 * Update the order attr of each section in the list to reflect the order of the collection
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_order: function() {

			var self = this;

			this.each( function( section ) {

				section.set( 'order', self.indexOf( section ) + 1 );

			} );

		},

	} );

} );

/**
 * Lessons Collection
 *
 * @since    3.13.0
 * @version  3.16.0
 */
define( 'Collections/loader',[
		'Collections/Lessons',
		'Collections/QuestionChoices',
		'Collections/Questions',
		'Collections/QuestionTypes',
		'Collections/Sections'
	], function(
		Lessons,
		QuestionChoices,
		Questions,
		QuestionTypes,
		Sections
	) {

		return {
			Lessons: Lessons,
			QuestionChoices: QuestionChoices,
			Questions: Questions,
			QuestionTypes: QuestionTypes,
			Sections: Sections,
		};

} );

/**
 * Abstract LifterLMS Model
 *
 * @since    3.17.0
 * @version  3.17.0
 */
define( 'Models/Abstract',[ 'Models/_Relationships', 'Models/_Utilities' ], function( Relationships, Utilities ) {

	return Backbone.Model.extend( _.defaults( {}, Relationships, Utilities ) );

} );

/**
 * Course Model.
 *
 * @since 3.16.0
 * @since 3.24.0 Added `get_total_points()` method.
 * @since 3.37.11 Use lesson author ID instead of author object when adding existing lessons to a course.
 * @version 5.4.0
 */
define( 'Models/Course',[ 'Collections/Sections', 'Models/_Relationships', 'Models/_Utilities' ], function( Sections, Relationships, Utilities ) {

	return Backbone.Model.extend( _.defaults( {

		relationships: {
			children: {
				sections: {
					class: 'Sections',
					model: 'section',
					type: 'collection',
				},
			}
		},

		/**
		 * New Course Defaults.
		 *
		 * @since 3.16.0
		 *
		 * @return {Object}
		 */
		defaults: function() {
			return {
				edit_url: '',
				sections: [],
				title: 'New Course',
				type: 'course',
				view_url: '',
			}
		},

		/**
		 * Init.
		 *
		 * @since 3.16.0
		 *
		 * @return {Void}
		 */
		initialize: function() {

			this.startTracking();
			this.init_relationships();

			// Sidebar "New Section" button broadcast.
			Backbone.pubSub.on( 'add-new-section', this.add_section, this );

			// Sidebar "New Lesson" button broadcast.
			Backbone.pubSub.on( 'add-new-lesson', this.add_lesson, this );

			Backbone.pubSub.on( 'lesson-search-select', this.add_existing_lesson, this );

		},

		/**
		 * Add an existing lesson to the course.
		 *
		 * Duplicate a lesson from this or another course or attach an orphaned lesson.
		 *
		 * @since 3.16.0
		 * @since 3.24.0 Unknown.
		 * @since 3.37.11 Use the author id instead of the author object.
		 * @since 5.4.0 Added filter hook 'llms_adding_existing_lesson_data'.
		 *               On cloning, duplicate assignments too, if assignment add-on active and assignment attached.
		 *
		 * @param {Object} lesson Lesson data obj.
		 * @return {Void}
		 */
		add_existing_lesson: function( lesson ) {

			var data = lesson.data;

			if ( 'clone' === lesson.action ) {

				delete data.id;

				// If a quiz is attached, duplicate the quiz also.
				if ( data.quiz ) {
					data.quiz                   = _.prepareQuizObjectForCloning( data.quiz );
					data.quiz._questions_loaded = true;
				}

				// If assignment add-on active and assignment attached, duplicate the assignment too.
				if ( window.llms_builder.assignments && data.assignment ) {
					data.assignment = _.prepareAssignmentObjectForCloning( data.assignment );
				}

			} else {

				data._forceSync = true;

			}

			delete data.order;
			delete data.parent_course;
			delete data.parent_section;

			// Use author id instead of the lesson author object.
			data = _.prepareExistingPostObjectDataForAddingOrCloning( data );

			/**
			 * Filters the data of the existing lesson being added.
			 *
			 * @since 5.4.0
			 *
			 * @param {Object} data   Lesson data.
			 * @param {String} action Action being performed. [clone|attach].
			 * @param {Object} course The lesson's course parent model.
			 */
			data = window.llms.hooks.applyFilters( 'llms_adding_existing_lesson_data', data, lesson.action, this );

			this.add_lesson( data );

		},

		/**
		 * Add a new lesson to the course.
		 *
		 * @since 3.16.0
		 *
		 * @param {Object} data Lesson data.
		 * @return {Object} Backbone.Model of the lesson.
		 */
		add_lesson: function( data ) {

			data        = data || {};
			var options = {},
				section;

			if ( ! data.parent_section ) {
				section = this.get_selected_section();
				if ( ! section ) {
					section = this.get( 'sections' ).last();
				}
			} else {
				section = this.get( 'sections' ).get( data.parent_section );
			}

			data._selected = true;

			data.parent_course = this.get( 'id' );

			var lesson = section.add_lesson( data, options );
			Backbone.pubSub.trigger( 'new-lesson-added', lesson );

			// Expand the section.
			section.set( '_expanded', true );

			return lesson;

		},

		/**
		 * Add a new section to the course.
		 *
		 * @since 3.16.0
		 *
		 * @param {Object} data Section data.
		 * @return {Void}
		 */
		add_section: function( data ) {

			data         = data || {};
			var sections = this.get( 'sections' ),
				options  = {},
				selected = this.get_selected_section();

			// If a section is selected, add the new section after the currently selected one.
			if ( selected ) {
				options.at = sections.indexOf( selected ) + 1;
			}

			sections.add( data, options );

		},

		/**
		 * Retrieve the currently selected section in the course.
		 *
		 * @since 3.16.0
		 *
		 * @return {Object|undefined}
		 */
		get_selected_section: function() {

			return this.get( 'sections' ).find( function( model ) {
				return model.get( '_selected' );
			} );

		},

		/**
		 * Retrieve the total number of points in the course.
		 *
		 * @since 3.24.0
		 *
		 * @return {Integer}
		 */
		get_total_points: function() {

			var points = 0;

			this.get( 'sections' ).each( function( section ) {
				section.get( 'lessons' ).each( function( lesson ) {
					var lesson_points = lesson.get( 'points' );
					if ( ! _.isNumber( lesson_points ) ) {
						lesson_points = 0;
					}
					points += lesson_points * 1;
				} );
			} );

			return points;

		},

	}, Relationships, Utilities ) );

} );

/**
 * Load all models
 *
 * @return   obj
 * @since    3.16.0
 * @version  3.17.0
 */
define( 'Models/loader',[
		'Models/Abstract',
		'Models/Course',
		'Models/Image',
		'Models/Lesson',
		'Models/Question',
		'Models/QuestionChoice',
		'Models/QuestionType',
		'Models/Quiz',
		'Models/Section'
	],
	function(
		Abstract,
		Course,
		Image,
		Lesson,
		Question,
		QuestionChoice,
		QuestionType,
		Quiz,
		Section
	) {

		return {
			Abstract: Abstract,
			Course: Course,
			Image: Image,
			Lesson: Lesson,
			Question: Question,
			QuestionChoice: QuestionChoice,
			QuestionType: QuestionType,
			Quiz: Quiz,
			Section: Section,
		};

} );

/**
 * Detachable model
 *
 * @package LifterLMS/Scripts
 *
 * @since    3.16.12
 * @version  3.16.12
 */

define( 'Views/_Detachable',[], function() {

	return {

		/**
		 * DOM Events
		 *
		 * @type  {Object}
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		events: {
			'click a[href="#llms-detach-model"]': 'detach_model',
		},

		/**
		 * Detaches a model from it's parent (doesn't delete)
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		detach_model: function( event ) {

			if ( event ) {
				event.preventDefault();
				event.stopPropagation();
			}

			var msg = LLMS.l10n.replace( 'Are you sure you want to detach this %s?', {
				'%s': this.model.get_l10n_type(),
			} );

			if ( window.confirm( msg ) ) {

				if ( this.model.collection ) {
					this.model.collection.remove( this.model );
				}

				// publish global event
				Backbone.pubSub.trigger( 'model-detached', this.model );

				// trigger local event so extending views can run other actions where necessary
				this.trigger( 'model-trashed', this.model );

			}

		},

	}

} );

/**
 * Handles UX and Events for inline editing of views
 *
 * Use with a Model's View
 * Allows editing model.title field via .llms-editable-title elements
 *
 * @package LifterLMS/Scripts
 *
 * @since 3.16.0
 * @since 3.25.4 Unknown
 * @since 3.37.11 Replace reference to `wp.editor` with `_.getEditor()` helper.
 * @version 3.37.11
 */
define( 'Views/_Editable',[], function() {

	return {

		media_lib: null,

		/**
		 * DOM Events
		 *
		 * @type  {Object}
		 * @since    3.16.0
		 * @version  3.17.8
		 */
		events: {
			'click .llms-add-image': 'open_media_lib',
			'click a[href="#llms-edit-slug"]': 'make_slug_editable',
			'click a[href="#llms-remove-image"]': 'remove_image',
			'change .llms-editable-select select': 'on_select',
			'change .llms-switch input[type="checkbox"]': 'toggle_switch',
			'change .llms-editable-radio input': 'on_radio_select',
			'focusin .llms-input': 'on_focus',
			'focusout .llms-input': 'on_blur',
			'keydown .llms-input': 'on_keydown',
			'input .llms-input[type="number"]': 'on_blur',
			'paste .llms-input[data-formatting]': 'on_paste',
		},

		/**
		 * Retrieve a list of allowed tags for a given element
		 *
		 * @param    obj   $el  jQuery selector for the element
		 * @return   array
		 * @since    3.16.0
		 * @version  3.17.8
		 */
		get_allowed_tags: function( $el ) {

			if ( $el.attr( 'data-formatting' ) ) {
				return _.map( $el.attr( 'data-formatting' ).split( ',' ), function( tag ) {
					return tag.trim();
				} );
			}

			return [ 'b', 'i', 'u', 'strong', 'em' ];

		},

		/**
		 * Retrieve the content of an element
		 *
		 * @param    obj   $el  jQuery object of the element
		 * @return   string
		 * @since    3.16.0
		 * @version  3.17.8
		 */
		get_content: function( $el ) {

			if ( 'INPUT' === $el[0].tagName ) {
				return $el.val();
			}

			if ( ! $el.attr( 'data-formatting' ) && ! $el.hasClass( 'ql-editor' ) ) {
				return $el.text();
			}

			return _.stripFormatting( $el.html(), this.get_allowed_tags( $el ) );

		},

		/**
		 * Determine if changes have been made to the element
		 *
		 * @param    {[obj]}   event  js event object
		 * @return   {Boolean}        true when changes have been made, false otherwise
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		has_changed: function( event ) {
			var $el = $( event.target );
			return ( $el.attr( 'data-original-content' ) !== this.get_content( $el ) );
		},

		/**
		 * Ensure that new content is at least 1 character long
		 *
		 * @param    obj   event  js event object
		 * @return   boolean
		 * @since    3.16.0
		 * @version  3.17.2
		 */
		is_valid: function( event ) {

			var self    = this,
				$el     = $( event.target ),
				content = this.get_content( $el ),
				type    = $el.attr( 'data-type' );

			if ( ( $el.attr( 'required' ) || $el.attr( 'data-required' ) ) && content.length < 1 ) {
				return false;
			}

			if ( 'url' === type || 'video' === type ) {
				if ( ! this._validate_url( this.get_content( $el ) ) ) {
					return false;
				}

			} else if ( 'permalink' === type ) {

				LLMS.Ajax.call( {
					data: {
						action: 'llms_builder',
						action_type: 'get_permalink',
						course_id: window.llms_builder.CourseModel.get( 'id' ),
						id: self.model.get( 'id' ),
						title: self.model.get( 'title' ),
						slug: content,
					},
					beforeSend: function() {
						LLMS.Spinner.start( $el.closest( '.llms-editable-toggle-group' ), 'small' );
					},
					success: function( r ) {

						if ( r.permalink && r.slug ) {
							self.model.set( 'permalink', r.permalink );
							self.model.set( 'name', r.slug );
							self.render();
						}

					}
				} );

			}

			return true;

		},

		/**
		 * Initialize datepicker elements
		 *
		 * @return   void
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		init_datepickers: function() {

			this.$el.find( '.llms-editable-date input' ).each( function() {

				$( this ).datetimepicker( {
					format: $( this ).attr( 'data-date-format' ) || 'Y-m-d h:i A',
					datepicker: ( undefined === $( this ).attr( 'data-date-datepicker' ) ) ? true : ( 'true' == $( this ).attr( 'data-date-datepicker' ) ),
					timepicker: ( undefined === $( this ).attr( 'data-date-timepicker' ) ) ? true : ( 'true' == $( this ).attr( 'data-date-timepicker' ) ),
					onClose: function( current_time, $input ) {
						$input.blur();
					}
				} );

			} );

		},

		/**
		 * Initialize elements that allow inline formatting
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		init_formatting_els: function() {

			var self = this;

			this.$el.find( '.llms-input-formatting[data-formatting]' ).each( function() {

				var formatting = $( this ).attr( 'data-formatting' ).split( ',' ),
					attr       = $( this ).attr( 'data-attribute' );

				var ed = new Quill( this, {
					modules: {
						toolbar: [ formatting ],
						keyboard: {
							bindings: {
								tab: {
									key: 9,
									handler: function( range, context ) {
										return true;
									},
								},
								13: {
									key: 13,
									handler: function( range, context ) {
										ed.root.blur();
										return false;
									},
								},
							},
						},
					},
					placeholder: $( this ).attr( 'data-placeholder' ),
					theme: 'bubble',
				} );

				ed.on( 'text-change', function( delta, oldDelta, source ) {
					self.model.set( attr, self.get_content( $( ed.root ) ) );
				} );

				Backbone.pubSub.trigger( 'formatting-ed-init', ed, $( this ), self );

			} );

		},

		/**
		 * Initialize editable select elements
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.25.4
		 */
		init_selects: function() {

			this.$el.find( '.llms-editable-select select' ).llmsSelect2( {
				width: '100%',
			} ).trigger( 'change' );

		},

		/**
		 * Blur/focusout function for .llms-editable-title elements
		 * Automatically saves changes if changes have been made
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.6
		 */
		on_blur: function( event ) {

			event.stopPropagation();

			this.model.set( '_has_focus', false, { silent: true } );

			var self    = this,
				$el     = $( event.target ),
				changed = this.has_changed( event );

			if ( changed ) {

				if ( ! self.is_valid( event ) ) {
					self.revert_edits( event );
				} else {
					this.save_edits( event );
				}

			}

		},

		/**
		 * Focus event for editable inputs
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.6
		 * @version  3.16.6
		 */
		on_focus: function( event ) {

			event.stopPropagation();
			this.model.set( '_has_focus', true, { silent: true } );

		},

		/**
		 * Handle content pasted into contenteditable fields
		 * This will ensure that HTML from RTF editors isn't pasted into the dom
		 *
		 * @param    obj   event  js event obj
		 * @return   void
		 * @since    3.17.8
		 * @version  3.17.8
		 */
		on_paste: function( event ) {

			event.preventDefault();
			event.stopPropagation();

			var text = ( event.originalEvent || event ).clipboardData.getData( 'text/plain' );
			window.document.execCommand( 'insertText', false, text );

		},

		/**
		 * Change event for selectables
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_select: function( event ) {

			var $el       = $( event.target ),
				multi     = ( $el.attr( 'multiple' ) ),
				attr      = $el.attr( 'name' ),
				$selected = $el.find( 'option:selected' ),
				val;

			if ( multi ) {
				val = [];
				val = $selected.map( function() {
					return this.value;
				} ).get();
			} else {
				val = $selected[0].value;
			}

			this.model.set( attr, val );

		},

		/**
		 * Change event for radio element groups
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.17.6
		 * @version  3.17.6
		 */
		on_radio_select: function( event ) {

			var $el  = $( event.target ),
				attr = $el.attr( 'name' ),
				val  = $el.val();

			this.model.set( attr, val );

		},

		/**
		 * Keydown function for .llms-editable-title elements
		 * Blurs
		 *
		 * @param    {obj}   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.17.8
		 */
		on_keydown: function( event ) {

			event.stopPropagation();

			var self  = this,
				key   = event.which || event.keyCode,
				shift = event.shiftKey;
				// ctrl = event.metaKey || event.ctrlKey;

			switch ( key ) {

				case 13: // enter
					// shift + enter should add a return
					if ( ! shift ) {
						event.preventDefault();
						event.target.blur();
					}
				break;

				case 27: // escape
					event.preventDefault();
					this.revert_edits( event );
					event.target.blur();
				break;

			}

		},

		/**
		 * Open the WP media lib
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.6
		 */
		open_media_lib: function( event ) {

			event.stopPropagation();

			var self = this,
				$el  = $( event.currentTarget );

			if ( self.media_lib ) {

				self.media_lib.uploader.uploader.param( 'post_id' );

			} else {

				self.media_lib = wp.media.frames.file_frame = wp.media( {
					title: LLMS.l10n.translate( 'Select an image' ),
					button: {
						text: LLMS.l10n.translate( 'Use this image' ),
					},
					multiple: false	// Set to true to allow multiple files to be selected
				} );

				self.media_lib.on( 'select', function() {

					var size       = $el.attr( 'data-image-size' ),
						attachment = self.media_lib.state().get( 'selection' ).first().toJSON(),
						image      = self.model.get( $el.attr( 'data-attribute' ) ),
						url;

					if ( size && attachment.sizes[ size ] ) {
						url = attachment.sizes[ size ].url;
					} else {
						url = attachment.url;
					}

					image.set( {
						id: attachment.id,
						src: url,
					} );

				} );

			}

			self.media_lib.open();

		},

		/**
		 * Click event to remove an image
		 *
		 * @param    obj   event  js event obj
		 * @return   voids
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		remove_image: function( event ) {

			event.preventDefault();

			this.model.get( $( event.currentTarget ).attr( 'data-attribute' ) ).set( {
				id: '',
				src: '',
			} );

		},

		/**
		 * Helper to undo changes
		 * Bound to "escape" key via on_keydown function
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		revert_edits: function( event ) {
			var $el = $( event.target ),
				val = $el.attr( 'data-original-content' );
			$el.html( val );
		},

		/**
		 * Sync changes to the model and DB
		 *
		 * @param    {obj}   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		save_edits: function( event ) {

			var $el = $( event.target ),
				val = this.get_content( $el );

			this.model.set( $el.attr( 'data-attribute' ), val );

		},

		/**
		 * Change event for a switch element
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.17.0
		 */
		toggle_switch: function( event ) {

			event.stopPropagation();
			var $el      = $( event.target ),
				attr     = $el.attr( 'name' ),
				rerender = $el.attr( 'data-rerender' ),
				val;

			if ( $el.is( ':checked' ) ) {
				val = $el.attr( 'data-on' ) ? $el.attr( 'data-on' ) : 'yes';
			} else {
				val = $el.attr( 'data-off' ) ? $el.attr( 'data-off' ) : 'no';
			}

			if ( -1 !== attr.indexOf( '.' ) ) {

				var split = attr.split( '.' );

				if ( 'parent' === split[0] ) {
					this.model.get_parent().set( split[1], val );
				} else {
					this.model.get( split[0] ).set( split[1], val );
				}

			} else {

				this.model.set( attr, val );

			}

			this.trigger( attr.replace( '.', '-' ) + '_toggle', val );

			if ( ! rerender || 'yes' === rerender ) {
				var self = this;
				setTimeout( function() {
					self.render();
				}, 100 );
			}

		},

		/**
		 * Initializes a WP Editor on a textarea
		 *
		 * @since 3.16.0
		 * @since 3.37.11 Replace reference to `wp.editor` with `_.getEditor()` helper.
		 *
		 * @param {String} id        CSS ID of the editor (don't include #).
		 * @param {Object} settings  Optional object of settings to pass to wp.oldEditor.initialize().
		 * @return {Void}
		 */
		init_editor: function( id, settings ) {

			settings = settings || {};

			var editor = _.getEditor();

			editor.remove( id );

			editor.initialize( id, $.extend( true, editor.getDefaultSettings(), {
				mediaButtons: true,
				tinymce: {
					toolbar1: 'bold,italic,strikethrough,bullist,numlist,blockquote,hr,alignleft,aligncenter,alignright,link,unlink,wp_adv',
					toolbar2: 'formatselect,underline,alignjustify,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
					setup: _.bind( this.on_editor_ready, this ),
				}
			}, settings ) );

		},

		/**
		 * Setup a permalink editor to allow editing of a permalink
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.6
		 * @version  3.16.6
		 */
		make_slug_editable: function( event ) {

			var self      = this,
				$btn      = $( event.currentTarget ),
				$link     = $btn.prevAll( 'a' ),
				$input    = $btn.prev( 'input.permalink' ),
				full_url  = $link.attr( 'href' ),
				slug      = $input.val(),
				short_url = full_url.replace( slug, '' );

			// hide the button
			$btn.hide();

			// make the link not clickable
			$link.css( {
				color: '#999',
				'pointer-events': 'none',
				'text-decoration': 'none',
			} );

			// remove the current slug & trailing slash from the URL
			$link.text( short_url.substring( 0, short_url.length - 1 ) );

			// focus in on the field
			$input.show().focus();

		},

		/**
		 * Callback function called after initialization of an editor
		 *
		 * Updates UI if a label is present.
		 *
		 * Binds a change event to ensure editor changes are saved to the model.
		 *
		 * @since 3.16.0
		 * @since 3.17.1 Uknown.
		 * @since 3.37.11 Replace references to `wp.editor` with `_.getEditor()` helper.
		 *
		 * @param {Object} editor TinyMCE Editor instance.
		 * @return {Void}
		 */
		on_editor_ready: function( editor ) {

			var self    = this,
				$ed     = $( '#' + editor.id ),
				$parent = $ed.closest( '.llms-editable-editor' ),
				$label  = $parent.find( '.llms-label' ),
				prop    = $ed.attr( 'data-attribute' )

			if ( $label.length ) {
				$label.prependTo( $parent.find( '.wp-editor-tools' ) );
			}

			// save changes to the model via Visual ed
			editor.on( 'change', function( event ) {
				self.model.set( prop, _.getEditor().getContent( editor.id ) );
			} );

			// save changes via Text ed
			$ed.on( 'input', function( event ) {
				self.model.set( prop, $ed.val() );
			} );

			// trigger an input on the Text ed when quicktags buttons are clicked
			$parent.on( 'click', '.quicktags-toolbar .ed_button', function() {
				setTimeout( function() {
					$ed.trigger( 'input' );
				}, 10 );
			} );

		},

		_validate_url: function( str ) {

			var a  = document.createElement( 'a' );
			a.href = str;
			return ( a.host && a.host !== window.location.host );

		}

	};

} );

/**
 * _receive override for Backbone.CollectionView core
 * enables connection with jQuery UI draggable buttons
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/_Receivable',[], function() {

	return {

		/**
		 * Overloads the function from Backbone.CollectionView core because it doesn't properly handle
		 * receives from a jQuery UI draggable object
		 *
		 * @param    obj   event  js event object
		 * @param    obj   ui     jQuery UI object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_receive : function( event, ui ) {

			// came from sidebar drag
			if ( ui.sender.hasClass( 'ui-draggable' ) ) {
				var index = this._getContainerEl().children().index( ui.helper );
				ui.helper.remove(); // remove the helper
				this.collection.add( {}, { at: index } );
				return;
			}

			var senderListEl             = ui.sender;
			var senderCollectionListView = senderListEl.data( 'view' );
			if ( ! senderCollectionListView || ! senderCollectionListView.collection ) {
				return;
			}

			var newIndex      = this._getContainerEl().children().index( ui.item );
			var modelReceived = senderCollectionListView.collection.get( ui.item.attr( 'data-model-cid' ) );
			senderCollectionListView.collection.remove( modelReceived );
			this.collection.add( modelReceived, { at : newIndex } );
			modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
			this.setSelectedModel( modelReceived );
		},

	}

} );

/**
 * Shiftable view mixin function
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/_Shiftable',[], function() {

	return {

		/**
		 * Conditionally hide action buttons based on section position in collection
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		maybe_hide_shiftable_buttons: function() {

			if ( ! this.model.collection ) {
				return;
			}

			var type = this.model.get( 'type' );

			if ( this.model.collection.first() === this.model ) {
				this.$el.find( '.shift-up--' + type ).hide();
			} else if ( this.model.collection.last() === this.model ) {
				this.$el.find( '.shift-down--' + type ).hide();
			}

		},

		/**
		 * Move an item in a collection from one position to another
		 *
		 * @param    int   old_index  current (old) index within the collection
		 * @param    int   new_index  desired (new) index within the collection
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		shift: function( old_index, new_index ) {

			var collection = this.model.collection;

			collection.remove( this.model );
			collection.add( this.model, { at: new_index } );
			collection.trigger( 'reorder' );

		},

		/**
		 * Move an item down the tree one position
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		shift_down: function( e ) {

			e.preventDefault();
			var index = this.model.collection.indexOf( this.model );
			this.shift( index, index + 1 );

		},

		/**
		 * Move an item up the tree one position
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		shift_up: function( e ) {

			e.preventDefault();
			var index = this.model.collection.indexOf( this.model );
			this.shift( index, index - 1 );

		},

	};

} );

/**
 * Subview utility mixin
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/_Subview',[], function() {

	return {

		subscriptions: {},

		/**
		 * Name of the current subview
		 *
		 * @type  {String}
		 */
		state: '',

		/**
		 * Object of subview data
		 *
		 * @type  {Object}
		 */
		views: {},

		/**
		 * Retrieve a subview by name from this.views
		 *
		 * @param    string   name   name of the subview
		 * @return   obl|false
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		get_subview: function( name ) {

			if ( this.views[ name ] ) {
				return this.views[ name ];
			}

			return false;

		},

		events_subscribe: function( events ) {

			_.each( events, function( func, event ) {

				this.subscriptions[ event ] = func;
				Backbone.pubSub.on( event, func, this );

			}, this );

		},

		events_unsubscribe: function() {

			_.each( this.subscriptions, function( func, event ) {

				Backbone.pubSub.off( event, func, this );
				delete this.subscriptions[ event ];

			}, this );

		},

		/**
		 * Remove a single subview (and all it's subviews) by name
		 *
		 * @param    string   name   name of the subview
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		remove_subview: function( name ) {

			var view = this.get_subview( name );

			if ( ! view ) {
				return;
			}

			if ( view.instance ) {

				// remove the subviews if the view has subviews
				if ( ! _.isEmpty( view.instance.views ) ) {
					view.instance.events_unsubscribe();
					view.instance.remove_subviews();
				}

				view.instance.off();
				view.instance.off( null, null, null );
				view.instance.remove();
				view.instance.undelegateEvents();

				// _.each( view.instance, function( val, key ) {
				// delete view.instance[ key ];
				// } );

				view.instance = null;

			}

		},

		/**
		 * Remove all subviews (and all the subviews of those subviews)
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		remove_subviews: function() {

			_.each( this.views, function( data, name ) {

				this.remove_subview( name );

			}, this );

		},

		/**
		 * Render subviews based on current state
		 *
		 * @param    obj   view_data  additional data to pass to the subviews
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render_subviews: function( view_data ) {

			view_data = view_data || {};

			_.each( this.views, function( data, name ) {

				if ( this.state === data.state ) {

					this.render_subview( name, view_data );

				} else {

					this.remove_subview( name );

				}

			}, this );

		},

		/**
		 * Render a single subview by name
		 *
		 * @param    string   name       name of the subview
		 * @param    obj      view_data  additional data to pass to the subview initializer
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render_subview: function( name, view_data ) {

			var view = this.get_subview( name );

			if ( ! view ) {
				return;
			}

			this.remove_subview( name );

			if ( ! view.instance ) {
				view.instance = new view.class( view_data );
			}

			view.instance.render();

		},

		/**
		 * Set the current subview
		 * Must call render after!
		 *
		 * @param    string   state  name of the state [builder|editor]
		 * @return   obj             this for chaining
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		set_state: function ( state ) {

			this.state = state;
			return this;

		},

	}

} );

/**
 * Trashable model
 *
 * @type     {Object}
 * @since    3.16.12
 * @version  3.16.12
 */
define( 'Views/_Trashable',[], function() {

	return {

		/**
		 * DOM Events
		 *
		 * @type  {Object}
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		events: {
			'click a[href="#llms-trash-model"]': 'trash_model',
		},

		/**
		 * Remove a model from it's parent and delete it
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		trash_model: function( event ) {

			if ( event ) {
				event.preventDefault();
				event.stopPropagation();
			}

			var msg = LLMS.l10n.replace( 'Are you sure you want to move this %s to the trash?', {
				'%s': this.model.get_l10n_type(),
			} );

			if ( window.confirm( msg ) ) {

				if ( this.model.collection ) {
					this.model.collection.remove( this.model );
				}

				// publish event
				Backbone.pubSub.trigger( 'model-trashed', this.model );

				// trigger local event so extending views can run other actions where necessary
				this.trigger( 'model-trashed', this.model );

			}

		},

	}

} );

/**
 * Load view mixins
 *
 * @package LifterLMS/Scripts
 *
 * @since    3.17.1
 * @version  3.17.1
 */

define( 'Views/_loader',[
		'Views/_Detachable',
		'Views/_Editable',
		'Views/_Receivable',
		'Views/_Shiftable',
		'Views/_Subview',
		'Views/_Trashable'
	],
	function(
		Detachable,
		Editable,
		Receivable,
		Shiftable,
		Subview,
		Trashable
	) {

		return {
			Detachable: Detachable,
			Editable: Editable,
			Receivable: Receivable,
			Shiftable: Shiftable,
			Subview: Subview,
			Trashable: Trashable,
		};

} );

/**
 * Constructor functions for constructing models, views, and collections
 *
 * @since    3.16.0
 * @version  3.17.1
 */
define( 'Controllers/Construct',[
		'Collections/loader',
		'Models/loader',
		'Views/_loader'
	], function(
		Collections,
		Models,
		Views
	) {

		return function() {

			/**
			 * Internal getter
			 * Constructs new Collections, Models, and Views
			 *
			 * @param    obj      type     type of object to construct [Collection,Model,View]
			 * @param    string   name     name of the object to construct
			 * @param    obj      data     object data to pass into the object's constructor
			 * @param    obj      options  object options to pass into the constructor
			 * @return   obj
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			function get( type, name, data, options ) {

				if ( ! type[ name ] ) {
					console.log( '"' + name + '" not found.' );
					return false;
				}

				return new type[ name ]( data, options );

			}

			/**
			 * Instantiate a collection
		 *
			 * @param    string   name     Collection class name (EG: "Sections")
			 * @param    array    data     Array of model objects to pass to the constructor
			 * @param    obj      options  Object of options to pass to the constructor
			 * @return   obj
			 * @since    3.17.0
			 * @version  3.17.0
			 */
			this.get_collection = function( name, data, options ) {

				return get( Collections, name, data, options );

			};

			/**
			 * Instantiate a model
			 *
			 * @param    string   name     Model class name (EG: "Section")
			 * @param    obj      data     Object of model attributes to pass to the constructor
			 * @param    obj      options  Object of options to pass to the constructor
			 * @return   obj
			 * @since    3.17.0
			 * @version  3.17.0
			 */
			this.get_model = function( name, data, options ) {

				return get( Models, name, data, options );

			};

			/**
			 * Let 3rd parties extend a view using any of the mixin (_) views
			 *
			 * @param    {obj}     view     base object used for the view
			 * @param... {string}  extends  any number of strings that should be mixed into the view
			 * @return   obj
			 * @since    3.17.1
			 * @version  3.17.1
			 */
			this.extend_view = function() {

				var view = arguments[0],
				i        = 1;

				while ( arguments[ i ] ) {

					var classname = arguments[ i ];
					if ( Views[ classname ] ) {

						if ( view.events && Views[ classname ].events ) {
							view.events = _.defaults( view.events, Views[ classname ].events );
						}

						view = _.defaults( view, Views[ classname ] );

					}

					i++;
				}

				return Backbone.View.extend( view );

			};

			/**
			 * Allows custom collection registration by extending the default BackBone collection
			 *
			 * @param    string   name   model name
			 * @param    obj      props  properties to extend the collection with
			 * @return   void
			 * @since    3.17.1
			 * @version  3.17.1
			 */
			this.register_collection = function( name, props ) {

				Collections[ name ] = Backbone.Collection.extend( props );

			};

			/**
			 * Allows custom model registration by extending the default abstract model
			 *
			 * @param    string   name   model name
			 * @param    obj      props  properties to extend the abstract model with
			 * @return   void
			 * @since    3.17.0
			 * @version  3.17.0
			 */
			this.register_model = function( name, props ) {

				Models[ name ] = Models['Abstract'].extend( props );

			};

			return this;

		};

} );

/**
 * LifterLMS Builder Debugging suite
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Controllers/Debug',[], function() {

	return function( settings ) {

		var self    = this,
			enabled = settings.enabled || false;

		/**
		 * Disable debugging
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		this.disable = function() {

			self.log( 'LifterLMS Builder debugging disabled' );
			enabled = false;

		};

		/**
		 * Enable debugging
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		this.enable = function() {

			enabled = true;
			self.log( 'LifterLMS Builder debugging enabled' );

		};

		/**
		 * General logging function
		 * Logs to the js console only if logging is enabled
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		this.log = function() {

			if ( ! enabled ) {
				return;
			}

			_.each( arguments, function( data ) {
				console.log( data );
			} );

		};

		/**
		 * Toggles current state of the logger on or off
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		this.toggle = function() {

			if ( enabled ) {
				self.disable();
			} else {
				self.enable();
			}

		};

		// on startup, log a message if logging is enabled
		if ( enabled ) {
			self.enable();
		}

	}

} );

/**
 * Model schema functions
 *
 * @since    3.17.0
 * @version  3.17.0
 */
define( 'Controllers/Schemas',[], function() {

	/**
	 * Main Schemas class
	 *
	 * @param    obj   schemas  schemas definitions initialized via PHP filters
	 * @return   obj
	 * @since    3.17.0
	 * @version  3.17.0
	 */
	return function( schemas ) {

		// initialize any custom schemas defined via PHP
		var custom_schemas = schemas;
		_.each( custom_schemas, function( type ) {
			_.each( type, function( schema ) {
				schema.custom = true;
			} );
		} );

		/**
		 * Retrieve a schema for a given model by type
		 * Extends default schemas definitions with custom 3rd party definitions
		 *
		 * @param    obj      schema      default schema definition from the model (or empty object if none defined)
		 * @param    string   model_type  the model type ('lesson', 'quiz', etc)
		 * @param    obj      model       Instance of the Backbone.Model for the given model
		 * @return   obj
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		this.get = function( schema, model_type, model ) {

			// extend the default schema with custom php schemas for the type if they exist
			if ( custom_schemas[ model_type ] ) {
				schema = _.extend( schema, custom_schemas[ model_type ] );
			}

			return schema;

		};

		return this;

	};

} );

/**
 * Sync builder data to the server
 *
 * @since 3.16.0
 * @version 4.17.0
 */
define( 'Controllers/Sync',[], function() {

	return function( Course, settings ) {

		this.saving = false;

		var self              = this,
			autosave          = ( 'yes' === window.llms_builder.autosave ),
			check_interval    = null,
			check_interval_ms = settings.check_interval_ms || 10000,
			detached          = new Backbone.Collection(),
			trashed           = new Backbone.Collection();

		/**
		 * init
		 *
		 * @since 3.16.7
		 *
		 * @return {Void}
		 */
		function init() {

			// determine if autosaving is possible
			if ( 'undefined' === typeof wp.heartbeat ) {

				window.llms_builder.debug.log( 'WordPress Heartbeat disabled. Autosaving is disabled!' );
				autosave = false;

			}

			// setup the check interval
			if ( check_interval_ms ) {
				self.set_check_interval( check_interval_ms );
			}

			// warn when users attempt to leave the page
			$( window ).on( 'beforeunload', function() {

				if ( self.has_unsaved_changes() ) {
					check_for_changes();
					return 'Are you sure you want to abandon your changes?';
				}

			} );

		};

		/*
			  /$$             /$$                                             /$$                           /$$
			 |__/            | $$                                            | $$                          |__/
			  /$$ /$$$$$$$  /$$$$$$    /$$$$$$   /$$$$$$  /$$$$$$$   /$$$$$$ | $$        /$$$$$$   /$$$$$$  /$$
			 | $$| $$__  $$|_  $$_/   /$$__  $$ /$$__  $$| $$__  $$ |____  $$| $$       |____  $$ /$$__  $$| $$
			 | $$| $$  \ $$  | $$    | $$$$$$$$| $$  \__/| $$  \ $$  /$$$$$$$| $$        /$$$$$$$| $$  \ $$| $$
			 | $$| $$  | $$  | $$ /$$| $$_____/| $$      | $$  | $$ /$$__  $$| $$       /$$__  $$| $$  | $$| $$
			 | $$| $$  | $$  |  $$$$/|  $$$$$$$| $$      | $$  | $$|  $$$$$$$| $$      |  $$$$$$$| $$$$$$$/| $$
			 |__/|__/  |__/   \___/   \_______/|__/      |__/  |__/ \_______/|__/       \_______/| $$____/ |__/
																								 | $$
																								 | $$
																								 |__/
		 */

		/**
		 * Adds error message(s) to the data object returned by heartbeat-tick
		 *
		 * @param    obj            data  llms_builder data object from heartbeat-tick
		 * @param    string|array   err   error messages array or string
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		function add_error_msg( data, err ) {

			if ( 'success' === data.status ) {
				data.message = [];
			}

			data.status = 'error';
			if ( 'string' === typeof err ) {
				err = [ err ];
			}
			data.message = data.message.concat( err );

			return data;

		};

		/**
		 * Publish sync status so other areas of the application can see what's happening here
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		function check_for_changes() {

			var data                 = {};
			data.changes             = self.get_unsaved_changes();
			data.has_unsaved_changes = self.has_unsaved_changes( data.changes );
			data.saving              = self.saving;

			window.llms_builder.debug.log( '==== start changes check ====', data, '==== finish changes check ====' );

			Backbone.pubSub.trigger( 'current-save-status', data );

		};

		/**
		 * Manually Save data via Admin AJAX when the heartbeat API has been disabled
		 *
		 * @since 3.16.7
		 * @since 4.17.0 Fixed undefined variable error when logging an error response.
		 *
		 * @return void
		 */
		function do_ajax_save() {

			// prevent simultaneous saves
			if ( self.saving ) {
				return;
			}

			var changes = self.get_unsaved_changes();

			// only send data if we have data to send
			if ( self.has_unsaved_changes( changes ) ) {

				changes.id = Course.get( 'id' );

				LLMS.Ajax.call( {
					data: {
						action: 'llms_builder',
						action_type: 'ajax_save',
						course_id: changes.id,
						llms_builder: JSON.stringify( changes ),
					},
					beforeSend: function() {

						window.llms_builder.debug.log( '==== start do_ajax_save before ====', changes, '==== finish do_ajax_save before ====' );

						self.saving = true;

						Backbone.pubSub.trigger( 'heartbeat-send', self );

					},
					error: function( xhr, status, error ) {

						window.llms_builder.debug.log( '==== start do_ajax_save error ====', xhr, '==== finish do_ajax_save error ====' );

						self.saving = false;

						Backbone.pubSub.trigger( 'heartbeat-tick', self, {
							status: 'error',
							message: xhr.responseText + ' (' + error + ' ' + status + ')',
						} );

					},
					success: function( res ) {

						if ( ! res.llms_builder ) {
							return;
						}

						window.llms_builder.debug.log( '==== start do_ajax_save success ====', res, '==== finish do_ajax_save success ====' );

						res.llms_builder = process_removals( res.llms_builder );
						res.llms_builder = process_updates( res.llms_builder );

						self.saving = false;

						Backbone.pubSub.trigger( 'heartbeat-tick', self, res.llms_builder );

					}

				} );

			}

		};

		/**
		 * Retrieve all the attributes changed on a model since the last sync
		 *
		 * For a new model (a model with a temp ID) or a model where _forceSync has been defined ALL atts will be returned
		 * For an existing model (without a temp ID) only retrieves changed attributes as tracked by Backbone.TrackIt
		 *
		 * This function excludes any attributes defined as child attributes via the models relationship settings
		 *
		 * @param    obj   model  instance of a Backbone.Model
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.6
		 */
		function get_changed_attributes( model ) {

			var atts = {},
				sync_type;

			// don't save mid editing
			if ( model.get( '_has_focus' ) ) {
				return atts;
			}

			// model hasn't been persisted to the database to get a real ID yet
			// send *all* of it's atts
			if ( has_temp_id( model ) || true === model.get( '_forceSync' ) ) {

				atts      = _.clone( model.attributes );
				sync_type = 'full';

				// only send the changed atts
			} else {

				atts      = model.unsavedAttributes();
				sync_type = 'partial';

			}

			var exclude = ( model.get_relationships ) ? model.get_child_props() : [];
			atts        = _.omit( atts, function( val, key ) {

				// exclude keys that start with an underscore which are used by the
				// application but don't need to be stored in the database
				if ( 0 === key.indexOf( '_' ) ) {
					return true;
				} else if ( -1 !== exclude.indexOf( key ) ) {
					return true;
				}
				return false;

			} );

			if ( model.before_save ) {
				atts = model.before_save( atts, sync_type );
			}

			return atts;

		};

		/**
		 * Get all the changes to an object (either a Model or a Collection of models)
		 * Returns only changes to models and the IDs of that model (should changes exist)
		 * Uses get_changed_attributes() to determine if all atts or only changed atts are needed
		 * Processes children intelligently to only return changed children rather than the entire collection of children
		 *
		 * @param    obj        object  instance of a Backbone.Model or Backbone.Collection
		 * @return   obj|array	  		if object is a model, returns an object
		 *                            	if object is a collection, returns an array of objects
		 * @since    3.16.0
		 * @version  3.16.11
		 */
		function get_changes_to_object( object ) {

			var changed_atts;

			if ( object instanceof Backbone.Model ) {

				changed_atts = get_changed_attributes( object );

				if ( object.get_relationships ) {

					_.each( object.get_child_props(), function( prop ) {

						var children = get_changes_to_object( object.get( prop ) );
						if ( ! _.isEmpty( children ) ) {
							changed_atts[ prop ] = children;
						}

					} );

				}

				// if we have any data, add the id to the model
				if ( ! _.isEmpty( changed_atts ) ) {
					changed_atts.id = object.get( 'id' );
				}

			} else if ( object instanceof Backbone.Collection ) {

				changed_atts = [];
				object.each( function( model ) {
					var model_changes = get_changes_to_object( model );
					if ( ! _.isEmpty( model_changes ) ) {
						changed_atts.push( model_changes );
					}
				} );

			}

			return changed_atts;

		};

		/**
		 * Determines if a model has a temporary ID or a real persisted ID
		 *
		 * @param    obj   model  instance of a model
		 * @return   boolean
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		function has_temp_id( model ) {

			return ( ! _.isNumber( model.id ) && 0 === model.id.indexOf( 'temp_' ) );

		};

		/**
		 * Compares changes synced to the server against current model and restarts
		 * tracking on elements that haven't changed since the last sync
		 *
		 * @param    obj   model  instance of a Backbone.Model
		 * @param    obj   data   data set that was processed by the server
		 * @return   void
		 * @since    3.16.11
		 * @version  3.19.4
		 */
		function maybe_restart_tracking( model, data ) {

			Backbone.pubSub.trigger( model.get( 'type' ) + '-maybe-restart-tracking', model, data );

			var omit = [ 'id', 'orig_id' ];

			if ( model.get_relationships ) {
				omit.concat( model.get_child_props() );
			}

			_.each( _.omit( data, omit ), function( val, prop ) {

				if ( _.isEqual( model.get( prop ), val ) ) {
					delete model._unsavedChanges[ prop ];
					model._originalAttrs[ prop ] = val;
				}

			} );

			// if syncing was forced, allow tracking to move forward as normal moving forward
			model.unset( '_forceSync' );

		};

		/**
		 * Processes response data from heartbeat-tick related to trashing & detaching models
		 * On success, removes from local removal collection
		 * On error, appends error messages to the data object returned to UI for on-screen feedback
		 *
		 * @param    obj   data  data.llms_builder object from heartbeat-tick response
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.17.1
		 */
		function process_removals( data ) {

			// check removals for errors
			var removals = {
				detach: detached,
				trash: trashed,
			};

			_.each( removals, function( coll, key ) {

				if ( data[ key ] ) {

					var errors = [];

					_.each( data[ key ] , function( info ) {

						// successfully detached, remove it from the detached collection
						if ( ! info.error ) {

							coll.remove( info.id );

						} else {

							errors.push( info.error );

						}

					} );

					if ( errors.length ) {
						_.extend( data, add_error_msg( data, errors ) );
					}

				}

			} );

			return data;
		}

		/**
		 * Processes response data from heartbeat-tick related to creating / updating a single object
		 * Handles both collections and models as a recursive function
		 *
		 * @param    {[type]}   data       [description]
		 * @param    {[type]}   type       [description]
		 * @param    {[type]}   parent     [description]
		 * @param    {[type]}   main_data  [description]
		 * @return   {[type]}
		 * @since    3.16.0
		 * @version  3.16.11
		 */
		function process_object_updates( data, type, parent, main_data ) {

			if ( ! data[ type ] ) {
				return data;
			}

			if ( parent.get( type ) instanceof Backbone.Model ) {

				var info = data[ type ];

				if ( info.error ) {

					_.extend( main_data, add_error_msg( main_data, info.error ) );

				} else {

					var model = parent.get( type );

					// update temp ids with the real id
					if ( info.id != info.orig_id ) {
						model.set( 'id', info.id );
						delete model._unsavedChanges.id;
					}
					maybe_restart_tracking( model, info );

					// check children
					if ( model.get_relationships ) {

						_.each( model.get_child_props(), function( child_key ) {
							_.extend( data[ type ], process_object_updates( data[ type ], child_key, model, main_data ) );
						} );

					}

				}

			} else if ( parent.get( type ) instanceof Backbone.Collection ) {

				_.each( data[ type ], function( info, index ) {

					if ( info.error ) {

						_.extend( main_data, add_error_msg( main_data, info.error ) );

					} else {

						var model = parent.get( type ).get( info.orig_id );

						// update temp ids with the real id
						if ( info.id != info.orig_id ) {
							model.set( 'id', info.id );
							delete model._unsavedChanges.id;
						}
						maybe_restart_tracking( model, info );

						// check children
						if ( model.get_relationships ) {

							_.each( model.get_child_props(), function( child_key ) {
								_.extend( data[ type ], process_object_updates( data[ type ][ index ], child_key, model, main_data ) );
							} );

						}

					}

				} );

			}

			return main_data;

		};

		/**
		 * Processes response data from heartbeat-tick related to updating & creating new models
		 * On success, removes from local removal collection
		 * On error, appends error messages to the data object returned to UI for on-screen feedback
		 *
		 * @param    obj   data  data.llms_builder object from heartbeat-tick response
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		function process_updates( data ) {

			// only mess with updates data
			if ( ! data.updates ) {
				return data;
			}

			if ( data.updates ) {
				data = process_object_updates( data.updates, 'sections', Course, data );
			}

			return data;

		};

		/*
								 /$$       /$$ /$$                                     /$$
								| $$      | $$|__/                                    |__/
			  /$$$$$$  /$$   /$$| $$$$$$$ | $$ /$$  /$$$$$$$        /$$$$$$   /$$$$$$  /$$
			 /$$__  $$| $$  | $$| $$__  $$| $$| $$ /$$_____/       |____  $$ /$$__  $$| $$
			| $$  \ $$| $$  | $$| $$  \ $$| $$| $$| $$              /$$$$$$$| $$  \ $$| $$
			| $$  | $$| $$  | $$| $$  | $$| $$| $$| $$             /$$__  $$| $$  | $$| $$
			| $$$$$$$/|  $$$$$$/| $$$$$$$/| $$| $$|  $$$$$$$      |  $$$$$$$| $$$$$$$/| $$
			| $$____/  \______/ |_______/ |__/|__/ \_______/       \_______/| $$____/ |__/
			| $$                                                            | $$
			| $$                                                            | $$
			|__/                                                            |__/
		*/

		/**
		 * Retrieve all unsaved changes for the builder instance
		 *
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.17.1
		 */
		this.get_unsaved_changes = function() {

			return {
				detach: detached.pluck( 'id' ),
				trash: trashed.pluck( 'id' ),
				updates: get_changes_to_object( Course ),

			}
		};

		/**
		 * Check if the builder instance has unsaved changes
		 *
		 * @param    obj      changes    optionally pass in an object from the return of this.get_unsaved_changes()
		 *                               save some resources by not running the check twice during heartbeats
		 * @return   boolean
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		this.has_unsaved_changes = function( changes ) {

			if ( 'undefined' === typeof changes ) {
				changes = self.get_unsaved_changes();
			}

			// check all possible keys, once we find one with content we have some changes to persist
			var found = _.find( changes, function( data ) {

				return ( false === _.isEmpty( data ) );

			} );

			return found ? true : false;

		};

		/**
		 * Save changes right now.
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.7
		 */
		this.save_now = function() {
			if ( autosave ) {
				wp.heartbeat.connectNow();
			} else {
				do_ajax_save();
			}
		};

		/**
		 * Update the interval that checks for changes to the builder instance
		 *
		 * @param    int        ms   time (in milliseconds) to run the check on
		 *                           pass 0 to disable the check
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		this.set_check_interval = function( ms ) {
			check_interval_ms = ms;
			if ( check_interval ) {
				clearInterval( check_interval );
			}
			if ( check_interval_ms ) {
				check_interval = setInterval( check_for_changes, check_interval_ms );
			}
		};

		/*
			 /$$ /$$             /$$
			| $$|__/            | $$
			| $$ /$$  /$$$$$$$ /$$$$$$    /$$$$$$  /$$$$$$$   /$$$$$$   /$$$$$$   /$$$$$$$
			| $$| $$ /$$_____/|_  $$_/   /$$__  $$| $$__  $$ /$$__  $$ /$$__  $$ /$$_____/
			| $$| $$|  $$$$$$   | $$    | $$$$$$$$| $$  \ $$| $$$$$$$$| $$  \__/|  $$$$$$
			| $$| $$ \____  $$  | $$ /$$| $$_____/| $$  | $$| $$_____/| $$       \____  $$
			| $$| $$ /$$$$$$$/  |  $$$$/|  $$$$$$$| $$  | $$|  $$$$$$$| $$       /$$$$$$$/
			|__/|__/|_______/    \___/   \_______/|__/  |__/ \_______/|__/      |_______/
		*/

		/**
		 * Listen for detached models and send them to the server for persistence
		 *
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		Backbone.pubSub.on( 'model-detached', function( model ) {

			// detached models with temp ids haven't been persisted so we don't care
			if ( has_temp_id( model ) ) {
				return;
			}

			detached.add( _.clone( model.attributes ) );

		} );

		/**
		 * Listen for trashed models and send them to the server for deletion
		 *
		 * @since    3.16.0
		 * @version  3.17.1
		 */
		Backbone.pubSub.on( 'model-trashed', function( model ) {

			// if the model has a temp ID we don't have to persist the deletion
			if ( has_temp_id( model ) ) {
				return;
			}

			var data = _.clone( model.attributes );

			if ( model.get_trash_id ) {
				data.id = model.get_trash_id();
			}

			trashed.add( data );

		} );

		/*
			 /$$                                       /$$     /$$                             /$$
			| $$                                      | $$    | $$                            | $$
			| $$$$$$$   /$$$$$$   /$$$$$$   /$$$$$$  /$$$$$$  | $$$$$$$   /$$$$$$   /$$$$$$  /$$$$$$
			| $$__  $$ /$$__  $$ |____  $$ /$$__  $$|_  $$_/  | $$__  $$ /$$__  $$ |____  $$|_  $$_/
			| $$  \ $$| $$$$$$$$  /$$$$$$$| $$  \__/  | $$    | $$  \ $$| $$$$$$$$  /$$$$$$$  | $$
			| $$  | $$| $$_____/ /$$__  $$| $$        | $$ /$$| $$  | $$| $$_____/ /$$__  $$  | $$ /$$
			| $$  | $$|  $$$$$$$|  $$$$$$$| $$        |  $$$$/| $$$$$$$/|  $$$$$$$|  $$$$$$$  |  $$$$/
			|__/  |__/ \_______/ \_______/|__/         \___/  |_______/  \_______/ \_______/   \___/
		*/


		/**
		 * Add data to the WP heartbeat to persist new models, changes, and deletions to the DB
		 *
		 * @since 3.16.0
		 * @since 3.16.7 Unknown
		 * @since 4.14.0 Return early when autosaving is disabled.
		 */
		$( document ).on( 'heartbeat-send', function( event, data ) {

			// Autosaving is disabled.
			if ( ! autosave ) {
				return;
			}

			// prevent simultaneous saves
			if ( self.saving ) {
				return;
			}

			var changes = self.get_unsaved_changes();

			// only send data if we have data to send
			if ( self.has_unsaved_changes( changes ) ) {

				changes.id        = Course.get( 'id' );
				self.saving       = true;
				data.llms_builder = JSON.stringify( changes );

			}

			window.llms_builder.debug.log( '==== start heartbeat-send ====', data, '==== finish heartbeat-send ====' );

			Backbone.pubSub.trigger( 'heartbeat-send', self );

		} );

		/**
		 * Confirm detachments & deletions and replace temp IDs with new persisted IDs
		 *
		 * @since 3.16.0
		 * @since 4.14.0 Return early when autosaving is disabled.
		 */
		$( document ).on( 'heartbeat-tick', function( event, data ) {

			// Autosaving is disabled.
			if ( ! autosave ) {
				return;
			}

			if ( ! data.llms_builder ) {
				return;
			}

			window.llms_builder.debug.log( '==== start heartbeat-tick ====', data, '==== finish heartbeat-tick ====' );

			data.llms_builder = process_removals( data.llms_builder );
			data.llms_builder = process_updates( data.llms_builder );

			self.saving = false;

			Backbone.pubSub.trigger( 'heartbeat-tick', self, data.llms_builder );

		} );

		/**
		 * On heartbeat errors publish an error to the main builder application
		 *
		 * @since 3.16.0
		 * @since 4.14.0 Return early when autosaving is disabled.
		 */
		$( document ).on( 'heartbeat-error', function( event, data ) {

			// Autosaving is disabled.
			if ( ! autosave ) {
				return;
			}

			window.llms_builder.debug.log( '==== start heartbeat-error ====', data, '==== finish heartbeat-error ====' );

			self.saving = false;

			Backbone.pubSub.trigger( 'heartbeat-tick', self, {
				status: 'error',
				message: data.responseText + ' (' + data.status + ' ' + data.statusText + ')',
			} );

		} );

		/*
			 /$$           /$$   /$$
			|__/          |__/  | $$
			 /$$ /$$$$$$$  /$$ /$$$$$$
			| $$| $$__  $$| $$|_  $$_/
			| $$| $$  \ $$| $$  | $$
			| $$| $$  | $$| $$  | $$ /$$
			| $$| $$  | $$| $$  |  $$$$/
			|__/|__/  |__/|__/   \___/
		*/
		init();

		return this;

	};

} );

/**
 * Single Lesson View
 * @since    3.16.0
 * @version  3.27.0
 */
define( 'Views/Lesson',[
		'Views/_Detachable',
		'Views/_Editable',
		'Views/_Shiftable',
		'Views/_Trashable'
	], function(
		Detachable,
		Editable,
		Shiftable,
		Trashable
	) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * Get default attributes for the html wrapper element
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		attributes: function() {
			return {
				'data-id': this.model.id,
				'data-section-id': this.model.get( 'parent_section' ),
			};
		},

		/**
		 * HTML class names
		 * @type  {String}
		 */
		className: 'llms-builder-item llms-lesson',

		/**
		 * Events
		 * @type  {Object}
		 * @since    3.16.0
		 * @version  3.16.12
		 */
		events: _.defaults( {
			'click .edit-lesson': 'open_lesson_editor',
			'click .edit-quiz': 'open_quiz_editor',
			'click .edit-assignment': 'open_assignment_editor',
			'click .section-prev': 'section_prev',
			'click .section-next': 'section_next',
			'click .shift-up--lesson': 'shift_up',
			'click .shift-down--lesson': 'shift_down',
		}, Detachable.events, Editable.events, Trashable.events ),

		/**
		 * HTML element wrapper ID attribute
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		id: function() {
			return 'llms-lesson-' + this.model.id;
		},

		/**
		 * Wrapper Tag name
		 * @type  {String}
		 */
		tagName: 'li',

		/**
		 * Get the underscore template
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-lesson-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 * @return   void
		 * @since    3.14.1
		 * @version  3.14.1
		 */
		initialize: function() {

			this.render();

			this.listenTo( this.model, 'change', this.render );

			Backbone.pubSub.on(  'lesson-selected', this.on_select, this );
			Backbone.pubSub.on(  'new-lesson-added', this.on_select, this );

		},

		/**
		 * Compiles the template and renders the view
		 * @return   self (for chaining)
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render: function() {

			this.$el.html( this.template( this.model ) );
			this.maybe_hide_shiftable_buttons();
			if ( this.model.get( '_selected' ) ) {
				this.$el.addClass( 'selected' );
			} else {
				this.$el.removeClass( 'selected' );
			}
			return this;

		},

		/**
		 * Click event for the assignment editor action icon
		 * Opens sidebar to the assignment editor tab
		 * @param    obj event JS Event obj.
		 * @return   void
		 * @since    3.17.0
		 * @version  3.27.0
		 */
		open_assignment_editor: function( event ) {

			if ( event ) {
				event.preventDefault();
			}

			Backbone.pubSub.trigger( 'lesson-selected', this.model, 'assignment' );
			this.model.set( '_selected', true );
			this.set_hash( 'assignment' );

		},

		/**
		 * Click event for lesson settings action icon
		 * Opens sidebar to the lesson editor tab
		 * @param    obj event JS Event obj.
		 * @return   void
		 * @since    3.16.0
		 * @version  3.27.0
		 */
		open_lesson_editor: function( event ) {

			if ( event ) {
				event.preventDefault();
			}

			Backbone.pubSub.trigger( 'lesson-selected', this.model, 'lesson' );
			this.model.set( '_selected', true );
			this.set_hash( false );

		},

		/**
		 * Click event for the quiz editor action icon
		 * Opens sidebar to the quiz editor tab
		 * @param    obj event JS Event obj.
		 * @return   void
		 * @since    3.16.0
		 * @version  3.27.0
		 */
		open_quiz_editor: function( event ) {

			if ( event ) {
				event.preventDefault();
			}

			Backbone.pubSub.trigger( 'lesson-selected', this.model, 'quiz' );
			this.model.set( '_selected', true );
			this.set_hash( 'quiz' );

		},

		/**
		 * When a lesson is selected mark it as selected in the hidden prop
		 * Allows views to re-render and reflect current state properly
		 * @param    obj   model  lesson model that's been selected
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_select: function( model ) {

			if ( this.model.id !== model.id ) {
				this.model.set( '_selected', false );
			}

		},

		/**
		 * Click event for the "Next Section" button
		 * @param    obj   event   js event obj
		 * @return   void
		 * @since    3.16.11
		 * @version  3.16.11
		 */
		section_next: function( event ) {
			event.preventDefault();
			this._move_to_section( 'next' );
		},

		/**
		 * Click event for the "Previous Section" button
		 * @param    obj   event   js event obj
		 * @return   void
		 * @since    3.16.11
		 * @version  3.16.11
		 */
		section_prev: function( event ) {
			event.preventDefault();
			this._move_to_section( 'prev' );
		},

		/**
		 * Adds a hash for deep linking to a specific lesson tab
		 * @param  string  subtab subtab [quiz|assignment]
		 * @return void
		 * @since   3.27.0
		 * @version 3.27.0
		 */
		set_hash: function( subtab ) {

			var hash = 'lesson:' + this.model.get( 'id' );

			if ( subtab ) {
				hash += ':' + subtab;
			}

			window.location.hash = hash;

		},

		/**
		 * Move the lesson into a new section
		 * @param    string   direction  direction [prev|next]
		 * @return   void
		 * @since    3.16.11
		 * @version  3.16.11
		 */
		_move_to_section: function( direction ) {

			var from_coll = this.model.collection,
				to_section;

			if ( 'next' === direction ) {
				to_section = from_coll.parent.get_next();
			} else if ( 'prev' === direction ) {
				to_section = from_coll.parent.get_prev();
			}

			if ( to_section ) {

				from_coll.remove( this.model );
				to_section.add_lesson( this.model );
				to_section.set( '_expanded', true );

			}

		},

	}, Detachable, Editable, Shiftable, Trashable ) );

} );

/**
 * Single Section View
 *
 * @since    3.13.0
 * @version  3.16.0
 */
define( 'Views/LessonList',[ 'Views/Lesson', 'Views/_Receivable' ], function( LessonView, Receivable ) {

	return Backbone.CollectionView.extend( _.defaults( {

		className: 'llms-lessons',

		/**
		 * Section model
		 *
		 * @type  {[type]}
		 */
		modelView: LessonView,

		/**
		 * Are sections selectable?
		 *
		 * @type  {Bool}
		 */
		selectable: false,

		/**
		 * Are sections sortable?
		 *
		 * @type  {Bool}
		 */
		sortable: true,

		sortableOptions: {
			axis: false,
			connectWith: '.llms-lessons',
			cursor: 'move',
			handle: '.drag-lesson',
			items: '.llms-lesson',
			placeholder: 'llms-lesson llms-sortable-placeholder',
		},

		sortable_start: function( collection ) {
			$( '.llms-lessons' ).addClass( 'dragging' );
		},

		sortable_stop: function( collection ) {
			$( '.llms-lessons' ).removeClass( 'dragging' );
		},

		/**
		 * Overloads the function from Backbone.CollectionView core because it doesn't send stop events
		 * if moving from one sortable to another... :-(
		 *
		 * @param    obj   event  js event object
		 * @param    obj   ui     jQuery UI object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_sortStop : function( event, ui ) {

			var modelBeingSorted     = this.collection.get( ui.item.attr( 'data-model-cid' ) ),
				modelViewContainerEl = this._getContainerEl(),
				newIndex             = modelViewContainerEl.children().index( ui.item );

			if ( newIndex == -1 && modelBeingSorted ) {
				this.collection.remove( modelBeingSorted );
			}

			this._reorderCollectionBasedOnHTML();
			this.updateDependentControls();

			if ( this._isBackboneCourierAvailable() ) {
				this.spawn( 'sortStop', { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
			} else {
				this.trigger( 'sortStop', modelBeingSorted, newIndex );
			}

		},

	}, Receivable ) );

} );

/**
 * Single Section View
 * @since    3.13.0
 * @version  3.16.12
 */
define( 'Views/Section',[
		'Views/LessonList',
		'Views/_Editable',
		'Views/_Shiftable',
		'Views/_Trashable'
	], function(
		LessonListView,
		Editable,
		Shiftable,
		Trashable
	) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * Get default attributes for the html wrapper element
		 * @return   obj
		 * @since    3.13.0
		 * @version  3.13.0
		 */
		attributes: function() {
			return {
				'data-id': this.model.id,
			};
		},

		/**
		 * Element class names
		 * @type  {String}
		 */
		className: 'llms-builder-item llms-section',

		/**
		 * Events
		 * @type     {Object}
		 * @since    3.16.0
		 * @version  3.16.12
		 */
		events: _.defaults( {

			'click': 'select',
			'click .expand': 'expand',
			'click .collapse': 'collapse',
			'click .shift-up--section': 'shift_up',
			'click .shift-down--section': 'shift_down',

			'mouseenter .llms-lessons': 'on_mouseenter',

		}, Editable.events, Trashable.events ),

		/**
		 * HTML element wrapper ID attribute
		 * @return   string
		 * @since    3.13.0
		 * @version  3.13.0
		 */
		id: function() {
			return 'llms-section-' + this.model.id;
		},

		/**
		 * Wrapper Tag name
		 * @type  {String}
		 */
		tagName: 'li',

		/**
		 * Get the underscore template
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-section-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 * @return   void
		 * @since    3.13.0
		 * @version  3.16.0
		 */
		initialize: function() {

			this.render();
			this.listenTo( this.model, 'change', this.render );
			this.listenTo( this.model, 'change:_expanded', this.toggle_expanded );
			this.lessonListView.collection.on( 'add', this.on_lesson_add, this );

			this.dragTimeout = null;

			Backbone.pubSub.on( 'expand-all', this.expand, this );
			Backbone.pubSub.on( 'collapse-all', this.collapse, this );

		},

		/**
		 * Render the section
		 * Initializes a new collection and views for all lessons in the section
		 * @return   void
		 * @since    3.13.0
		 * @version  3.16.0
		 */
		render: function() {

			this.$el.html( this.template( this.model.toJSON() ) );

			this.maybe_hide_shiftable_buttons();

			this.lessonListView = new LessonListView( {
				el: this.$el.find( '.llms-lessons' ),
				collection: this.model.get( 'lessons' ),
			} );
			this.lessonListView.render();
			this.lessonListView.on( 'sortStart', this.lessonListView.sortable_start );
			this.lessonListView.on( 'sortStop', this.lessonListView.sortable_stop );

			// selection changes
			this.lessonListView.on( 'selectionChanged', this.active_lesson_change, this );

			this.maybe_hide_trash_button();

			return this;

		},

		active_lesson_change: function( current, previous ) {

			Backbone.pubSub.trigger( 'active-lesson-change', {
				current: current,
				previous: previous,
			} );

		},

		/**
		 * Collapse lessons within the section
		 * @param    obj   event    js event object
		 * @param    bool  update   if true, updates the model to reflect the new state
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		collapse: function( event, update ) {

			if ( 'undefined' === typeof update ) {
				update = true;
			}

			if ( event ) {
				event.stopPropagation();
				event.preventDefault();
			}

			this.$el.removeClass( 'expanded' ).find( '.drag-expanded' ).removeClass( 'drag-expanded' );
			if ( update ) {
				this.model.set( '_expanded', false );
			}
			Backbone.pubSub.trigger( 'section-toggle', this.model );

		},

		/**
		 * Expand lessons within the section
		 * @param    obj   event    js event object
		 * @param    bool  update   if true, updates the model to reflect the new state
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		expand: function( event, update ) {

			if ( 'undefined' === typeof update ) {
				update = true;
			}

			if ( event ) {
				event.stopPropagation();
				event.preventDefault();
			}

			this.$el.addClass( 'expanded' );
			if ( update ) {
				this.model.set( '_expanded', true );
			}
			Backbone.pubSub.trigger( 'section-toggle', this.model );

		},

		maybe_hide_trash_button: function() {

			var $btn = this.$el.find( '.trash--section' );

			if ( this.model.get( 'lessons' ).isEmpty() ) {

				$btn.show();

			} else {

				$btn.hide()

			}

		},

		/**
		 * When a lesson is added to the section trigger a collection reorder & update the lesson's id
		 * @param    obj   model  Lesson model
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_lesson_add: function( model ) {

			this.lessonListView.collection.trigger( 'reorder' );
			model.set( 'parent_section', this.model.get( 'id' ) );
			this.expand();

		},

		on_mouseenter: function( event ) {


			if ( $( event.target ).hasClass( 'dragging' ) ) {

				$( '.drag-expanded' ).removeClass( 'drag-expanded' );
				$( event.target ).addClass( 'drag-expanded' );

			}

		},

		/**
		 * Expand
		 * @param    {[type]}   model  [description]
		 * @param    {[type]}   value  [description]
		 * @return   {[type]}
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		toggle_expanded: function( model, value ) {

			if ( value ) {
				this.expand( null, false );
			} else {
				this.collapse( null, false );
			}

		},

	}, Editable, Shiftable, Trashable ) );

} );

/**
 * Single Section View
 *
 * @since    3.13.0
 * @version  3.16.0
 */
define( 'Views/SectionList',[ 'Views/Section', 'Views/_Receivable' ], function( SectionView, Receivable ) {

	return Backbone.CollectionView.extend( _.defaults( {

		/**
		 * Parent element
		 *
		 * @type  {String}
		 */
		el: '#llms-sections',

		events : {
			'mousedown > li.llms-section > .llms-builder-header .llms-headline' : '_listItem_onMousedown',
			// 'dblclick > li, tbody > tr > td' : '_listItem_onDoubleClick',
			'click' : '_listBackground_onClick',
			'click ul.collection-view' : '_listBackground_onClick',
			'keydown' : '_onKeydown'
		},

		/**
		 * Section model
		 *
		 * @type  {[type]}
		 */
		modelView: SectionView,

		/**
		 * Enable keyboard events
		 *
		 * @type  {Bool}
		 */
		processKeyEvents: false,

		/**
		 * Are sections selectable?
		 *
		 * @type  {Bool}
		 */
		selectable: true,

		/**
		 * Are sections sortable?
		 *
		 * @type  {Bool}
		 */
		sortable: true,

		sortableOptions: {
			axis: false,
			cursor: 'move',
			handle: '.drag-section',
			items: '.llms-section',
			placeholder: 'llms-section llms-sortable-placeholder',
		},

		sortable_start: function( collection ) {
			this.$el.addClass( 'dragging' );
		},

		sortable_stop: function( collection ) {
			this.$el.removeClass( 'dragging' );
		},

	}, Receivable ) );

} );

/**
 * Single Course View
 *
 * @since    3.13.0
 * @version  3.16.0
 */
define( 'Views/Course',[ 'Views/SectionList', 'Views/_Editable' ], function( SectionListView, Editable ) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * Get default attributes for the html wrapper element
		 *
		 * @return   obj
		 * @since    3.13.0
		 * @version  3.13.0
		 */
		attributes: function() {
			return {
				'data-id': this.model.id,
			};
		},

		/**
		 * HTML element selector
		 *
		 * @type  {String}
		 */
		el: '#llms-builder-main',

		/**
		 * Wrapper Tag name
		 *
		 * @type  {String}
		 */
		tagName: 'div',

		/**
		 * Get the underscore template
		 *
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-course-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 *
		 * @return   void
		 * @since    3.13.0
		 * @version  3.13.0
		 */
		initialize: function() {

			var self = this;

			// this.listenTo( this.model, 'sync', this.render );
			this.render();

			this.sectionListView = new SectionListView( {
				collection: this.model.get( 'sections' ),
			} );
			this.sectionListView.render();
			// drag and drop start
			this.sectionListView.on( 'sortStart', this.sectionListView.sortable_start );
			// drag and drop stop
			this.sectionListView.on( 'sortStop', this.sectionListView.sortable_stop );
			// selection changes
			this.sectionListView.on( 'selectionChanged', this.active_section_change );
			// "select" a section when it's added to the course
			this.listenTo( this.model.get( 'sections' ), 'add', this.on_section_add );

			Backbone.pubSub.on( 'section-toggle', this.on_section_toggle, this );

			Backbone.pubSub.on( 'expand-section', this.expand_section, this );

			Backbone.pubSub.on( 'lesson-selected', this.active_lesson_change, this );

		},

		/**
		 * Compiles the template and renders the view
		 *
		 * @return   self (for chaining)
		 * @since    3.13.0
		 * @version  3.13.0
		 */
		render: function() {
			this.$el.html( this.template( this.model ) );
			return this;
		},

		active_lesson_change: function( model ) {

			// set parent section to be active
			var section = this.model.get( 'sections' ).get( model.get( 'parent_section' ) );
			this.sectionListView.setSelectedModel( section );

		},

		/**
		 * When a section "selection" changes in the list
		 * Update each section model so we can figure out which one is selected from other views
		 *
		 * @param    array   current   array of selected models
		 * @param    array   previous  array of previously selected models
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		active_section_change: function( current, previous ) {

			_.each( current, function( model ) {
				model.set( '_selected', true );
			} );

			_.each( previous, function( model ) {
				model.set( '_selected', false );
			} );

		},

		/**
		 * "Selects" the new section when it's added to the course
		 *
		 * @param    obj   model  Section model that's just been added
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_section_add: function( model ) {

			this.sectionListView.setSelectedModel( model );

		},

		/**
		 * When expanding/collapsing sections
		 * if collapsing, unselect, if expanding, select
		 *
		 * @param    obj   model  toggled section
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_section_toggle: function( model ) {

			var selected = model.get( '_expanded' ) ? [ model ] : [];
			this.sectionListView.setSelectedModels( selected );

		}

	}, Editable ) );

} );

/**
 * Model settings fields view
 *
 * @since 3.17.0
 * @version 4.7.0
 */
define( 'Views/SettingsFields',[], function() {

	return Backbone.View.extend( _.defaults( {

		/**
		 * DOM events
		 *
		 * @type  {Object}
		 */
		events: {
			'click .llms-settings-group-toggle': 'toggle_group',
		},

		/**
		 * Processed fields data
		 * Allows access by ID without traversing the schema
		 *
		 * @type  {Object}
		 */
		fields: {},

		/**
		 * Wrapper Tag name
		 *
		 * @type  {String}
		 */
		tagName: 'div',

		/**
		 * Get the underscore template
		 *
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-settings-fields-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 *
		 * @return   void
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		// initialize: function() {},

		/**
		 * Retrieve an array of all editor fields in all groups
		 *
		 * @return   array
		 * @since    3.17.1
		 * @version  3.17.1
		 */
		get_editor_fields: function() {
			return _.filter( this.fields, function( field ) {
				return this.is_editor_field( field.type );
			}, this );
		},

		/**
		 * Get settings group data from a model
		 *
		 * @return   {[type]}
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		get_groups: function() {

			return this.model.get_settings_fields();

		},

		/**
		 * Determine if a settings group is hidden in localStorage
		 *
		 * @param    string   group_id  id of the group
		 * @return   {Boolean}
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		is_group_hidden: function( group_id ) {

			var id = 'llms-' + this.model.get( 'type' ) + '-settings-group--' + group_id;

			if ( 'undefined' !== window.localStorage ) {
				return ( 'hidden' === window.localStorage.getItem( id ) );
			}

			return false;

		},

		/**
		 * Get the switch attribute for a field with switches
		 *
		 * @param    obj   field  field data obj
		 * @return   string
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		get_switch_attribute: function( field ) {

			return field.switch_attribute ? field.switch_attribute : field.attribute;

		},

		/**
		 * Determine if a field has a switch
		 *
		 * @param    string   type  field type string
		 * @return   {Boolean}
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		has_switch: function( type ) {
			return ( -1 !== type.indexOf( 'switch' ) );
		},

		/**
		 * Determine if a field is a default (text) field
		 *
		 * @param    string   type  field type string
		 * @return   {Boolean}
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		is_default_field: function( type ) {

			var types = [ 'audio_embed', 'datepicker', 'number', 'text', 'video_embed' ];
			return ( -1 !== types.indexOf( type.replace( 'switch-', '' ) ) );

		},

		/**
		 * Determine if a field is a WYSIWYG editor field
		 *
		 * @param    string   type  field type string
		 * @return   {Boolean}
		 * @since    3.17.1
		 * @version  3.17.1
		 */
		is_editor_field: function( type ) {

			var types = [ 'editor', 'switch-editor' ];
			return ( -1 !== types.indexOf( type.replace( 'switch-', '' ) ) );

		},

		/**
		 * Determine if a switch is enabled for a field
		 *
		 * @param    obj   field  field data object
		 * @return   {Boolean}
		 * @since    3.17.0
		 * @version  3.17.6
		 */
		is_switch_condition_met: function( field ) {

			return ( field.switch_on === this.model.get( field.switch_attribute ) );

		},

		/**
		 * Compiles the template and renders the view
		 *
		 * @return   self (for chaining)
		 * @since    3.17.0
		 * @version  3.17.1
		 */
		render: function() {

			this.$el.html( this.template( this ) );

			// if editors exist, render them
			_.each( this.get_editor_fields(), function( field ) {
				this.render_editor( field );
			}, this );

			return this;

		},

		/**
		 * Renders an editor field
		 *
		 * @since 3.17.1
		 * @since 3.37.11 Replace references to `wp.editor` with `_.getEditor()` helper.
		 *
		 * @param  {Object} field Field data object.
		 * @return {Void}
		 */
		render_editor: function( field ) {

			var self     = this,
				wpEditor = _.getEditor();

			// Exit early if there's no editor to work with.
			if ( undefined === wpEditor ) {
				console.error( 'Unable to access `wp.oldEditor` or `wp.editor`.' );
				return;
			}

			wpEditor.remove( field.id );
			field.settings.tinymce.setup = function( editor ) {

				var $ed     = $( '#' + editor.id ),
					$parent = $ed.closest( '.llms-editable-editor' ),
					$label  = $parent.find( '.llms-label' ),
					prop    = $ed.attr( 'data-attribute' )

				if ( $label.length ) {
					$label.prependTo( $parent.find( '.wp-editor-tools' ) );
				}

				// save changes to the model via Visual ed
				editor.on( 'change', function( event ) {
					self.model.set( prop, wpEditor.getContent( editor.id ) );
				} );

				// save changes via Text ed
				$ed.on( 'input', function( event ) {
					self.model.set( prop, $ed.val() );
				} );

				// trigger an input on the Text ed when quicktags buttons are clicked
				$parent.on( 'click', '.quicktags-toolbar .ed_button', function() {
					setTimeout( function() {
						$ed.trigger( 'input' );
					}, 10 );
				} );
			};

			wpEditor.initialize( field.id, field.settings );

		},

		/**
		 * Get the HTML for a select field
		 *
		 * @param    obj      options    flat or multi-dimensional options object
		 * @param    string   attribute  name of the select field's attribute
		 * @return   string
		 * @since    3.17.0
		 * @version  3.17.2
		 */
		render_select_options: function( options, attribute ) {

			var html     = '',
				selected = this.model.get( attribute );

			function option_html( label, val ) {

				return '<option value="' + val + '"' + _.selected( val, selected ) + '>' + label + '</option>';

			}

			_.each( options, function( option, index ) {

				// this will be an key:val object
				if ( 'string' === typeof option ) {
					html += option_html( option, index );
					// either option group or array of key,val objects
				} else if ( 'object' === typeof option ) {
					// option group
					if ( option.label && option.options ) {
						html += '<optgroup label="' + option.label + '">';
						html += this.render_select_options( option.options, attribute );
					} else {
						html += option_html( option.val, option.key );
					}
				}

			}, this );

			return html;

		},

		/**
		 * Setup and fill fields with default data based on field type
		 *
		 * @since 3.17.0
		 * @since 3.24.0 Unknown.
		 * @since 3.37.11 Replace reference to `wp.editor` with `_.getEditor()` helper.
		 * @since 4.7.0 Ensure `switch-number` fields are set with the `number` type attribute.
		 *
		 * @param  {Object}  orig_field  Original field as defined in the settings.
		 * @param  {Integer} field_index Index of the field in the current row.
		 * @return {Object}
		 */
		setup_field: function( orig_field, field_index ) {

			var defaults = {
				classes: [],
				id: _.uniqueId( orig_field.attribute + '_' ),
				input_type: 'text',
				label: '',
				options: {},
				placeholder: '',
				tip: '',
				tip_position: 'top-right',
				settings: {},
			};

			// check the field condition if set
			if ( orig_field.condition && false === _.bind( orig_field.condition, this.model )() ) {
				return false;
			}

			switch ( orig_field.type ) {

				case 'audio_embed':
					defaults.classes.push( 'llms-editable-audio' );
					defaults.placeholder = 'https://';
					defaults.tip         = LLMS.l10n.translate( 'Use SoundCloud or Spotify audio URLS.' );
					defaults.input_type  = 'url';
				break;

				case 'datepicker':
					defaults.classes.push( 'llms-editable-date' );
				break;

				case 'editor':
				case 'switch-editor':
					var orig_settings = orig_field.settings || {};
					defaults.settings = $.extend( true, _.getEditor().getDefaultSettings(), {
						mediaButtons: true,
						tinymce: {
							toolbar1: 'bold,italic,strikethrough,bullist,numlist,blockquote,hr,alignleft,aligncenter,alignright,link,unlink,wp_adv',
							toolbar2: 'formatselect,underline,alignjustify,forecolor,pastetext,removeformat,charmap,outdent,indent,undo,redo,wp_help',
						}
					}, orig_settings );
				break;

				case 'number':
				case 'switch-number':
					defaults.input_type = 'number';
				break;

				case 'permalink':
					defaults.label = LLMS.l10n.translate( 'Permalink' );
				break;

				case 'video_embed':
					defaults.classes.push( 'llms-editable-video' );
					defaults.placeholder = 'https://';
					defaults.tip         = LLMS.l10n.translate( 'Use YouTube, Vimeo, or Wistia video URLS.' );
					defaults.input_type  = 'url';
				break;

			}

			if ( this.has_switch( orig_field.type ) ) {
				defaults.switch_on  = 'yes';
				defaults.switch_off = 'no';
			}

			var field = _.defaults( _.deepClone( orig_field ), defaults );

			// if options is a function run it
			if ( _.isFunction( field.options ) ) {
				field.options = _.bind( field.options, this.model )();
			}

			// if it's a radio field options values can be submitted as images
			// this will transform those images into <img> html
			if ( -1 !== [ 'radio', 'switch-radio' ].indexOf( orig_field.type ) ) {

				var has_images = false;
				_.each( orig_field.options, function( val, key ) {
					if ( -1 !== val.indexOf( '.png' ) || -1 !== val.indexOf( '.jpg' ) ) {
						field.options[key] = '<span><img src="' + val + '"></span>';
						has_images         = true;
					}
				} );
				if ( has_images ) {
					field.classes.push( 'has-images' );
				}

			}

			// transform classes array to a css class string
			if ( field.classes.length ) {
				field.classes = ' ' + field.classes.join( ' ' );
			}

			this.fields[ field.id ] = field;

			return field;

		},

		/**
		 * Determine if toggling a switch select should rerender the view
		 *
		 * @param    string   field_type  field type string
		 * @return   boolean
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		should_rerender_on_toggle: function( field_type ) {

			return ( -1 !== field_type.indexOf( 'switch-' ) ) ? 'yes' : 'no';

		},

		/**
		 * Click event for toggling visibility of settings groups
		 * If localStorage is available, persist state
		 *
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.17.0
		 * @version  3.17.0
		 */
		toggle_group: function( event ) {

			event.preventDefault();

			var $el    = $( event.currentTarget ),
				$group = $el.closest( '.llms-model-settings' );

			$group.toggleClass( 'hidden' );

			if ( 'undefined' !== window.localStorage ) {

				var id = $group.attr( 'id' );
				if ( $group.hasClass( 'hidden' ) ) {
					window.localStorage.setItem( id, 'hidden' );
				} else {
					window.localStorage.removeItem( id );
				}

			}

		},

	} ) );

} );

/**
 * Lesson Editor (Sidebar) View
 *
 * @package LifterLMS/Scripts/Builder
 *
 * @since 3.17.0
 * @since 3.35.2 Added filter `llms_lesson_rerender_change_events` to view re-render change events.
 * @version 3.35.2
 */
define( 'Views/LessonEditor',[
		'Views/_Detachable',
		'Views/_Editable',
		'Views/_Trashable',
		'Views/_Subview',
		'Views/SettingsFields'
	], function(
		Detachable,
		Editable,
		Trashable,
		Subview,
		SettingsFields
	) {

		return Backbone.View.extend( _.defaults( {

			/**
			 * Current view state
			 *
			 * @type  {String}
			 */
			state: 'default',

			/**
			 * Current Subviews
			 *
			 * @type  {Object}
			 */
			views: {
				settings: {
					class: SettingsFields,
					instance: null,
					state: 'default',
				},
			},

			el: '#llms-editor-lesson',

			/**
			 * Events
			 *
			 * @type  {Object}
			 */
			events: _.defaults( {}, Detachable.events, Editable.events, Trashable.events ),

			/**
			 * Template function
			 *
			 * @type  {[type]}
			 */
			template: wp.template( 'llms-lesson-settings-template' ),

			/**
			 * Init
			 *
			 * @since 3.17.0
			 * @since 3.24.0 Unknown
			 * @since 3.35.2 Added filter to change events.
			 *
			 * @param {obj} data Parent template data.
			 * @return {void}
			 */
			initialize: function( data ) {

				this.model = data.lesson;

				var change_events = window.llms.hooks.applyFilters( 'llms_lesson_rerender_change_events', [
					'change:date_available',
					'change:drip_method',
					'change:time_available',
				] );
				_.each( change_events, function( event ) {
					this.listenTo( this.model, event, this.render );
				}, this );

				// render only the tooltip for points percentage when points change
				this.listenTo( this.model, 'change:points', this.render_points_percentage );

				// when the "has_prerequisite" attr is toggled ON
				// trigger the prereq select object to set the default (first available) prereq for the lesson
				this.listenTo( this.model, 'change:has_prerequisite', function( lesson, val ) {
					if ( 'yes' === val ) {
						this.$el.find( 'select[name="prerequisite"]' ).trigger( 'change' );
					}
				} );

			},

			/**
			 * Render the view
			 *
			 * @return   obj
			 * @since    3.17.0
			 * @version  3.24.0
			 */
			render: function() {

				this.$el.html( this.template( this.model ) );

				this.remove_subview( 'settings' );

				this.render_subview( 'settings', {
					el: '#llms-lesson-settings-fields',
					model: this.model,
				} );

				this.init_datepickers();
				this.init_selects();

				this.render_points_percentage();

				return this;

			},

			/**
			 * Render the portion of the template which displays the points percentage
			 *
			 * @return   void
			 * @since    3.24.0
			 * @version  3.24.0
			 */
			render_points_percentage: function() {
				this.$el.find( '#llms-model-settings-field--points .llms-editable-input' )
				.addClass( 'tip--top-left' )
				.attr( 'data-tip', this.model.get_points_percentage() );
			}

		}, Detachable, Editable, Trashable, Subview, SettingsFields ) );

} );

/**
 * Popover View
 *
 * @since 3.16.0
 * @version 4.0.0
 */
define( 'Views/Popover',[], function() {

	return Backbone.View.extend( {

		/**
		 * Default Properties
		 *
		 * @type {Object}
		 */
		defaults: {
			placement: 'auto',
			// container: document.body,
			width: 'auto',
			trigger: 'manual',
			style: 'light',
			animation: 'pop',
			title: '',
			content: '',
			closeable: false,
			backdrop: false,
			onShow: function( $el ) {},
			onHide: function( $el ) {},
		},

		/**
		 * Wrapper Tag name
		 *
		 * @type {String}
		 */
		tagName: 'div',

		/**
		 * Initialization callback func (renders the element on screen)
		 *
		 * @since 3.14.1
		 * @since 4.0.0 Add RTL support for popovers.
		 *
		 * @return void
		 */
		initialize: function( data ) {

			if ( this.$el.length ) {
				this.defaults.container = this.$el.parent();
			}

			this.args = _.defaults( data.args, this.defaults );

			// Reverse directions for RTL sites.
			if ( $( 'body' ).hasClass( 'rtl' ) ) {

				if ( -1 !== this.args.placement.indexOf( 'left' ) ) {
					this.args.placement = this.args.placement.replace( 'left', 'right' );
				} else if ( -1 !== this.args.placement.indexOf( 'right' ) ) {
					this.args.placement = this.args.placement.replace( 'right', 'left' );
				}

			}

			this.render();

		},

		/**
		 * Compiles the template and renders the view
		 *
		 * @since 3.16.0
		 *
		 * @return {Object} Instance of the Backbone.view.
		 */
		render: function() {

			this.$el.webuiPopover( this.args );
			return this;

		},

		/**
		 * Hide the popover
		 *
		 * @since 3.16.0
		 * @since 3.16.12 Unknown.
		 *
		 * @return {Object} Instance of the Backbone.view.
		 */
		hide: function() {

			this.$el.webuiPopover( 'hide' );
			return this;

		},

		/**
		 * Show the popover
		 *
		 * @since 3.16.0
		 * @since 3.16.12 Unknown.
		 *
		 * @return {Object} Instance of the Backbone.view.
		 */
		show: function() {

			this.$el.webuiPopover( 'show' );
			return this;

		},

	} );

} );

/**
 * Post Popover Search content View
 *
 * @since 3.16.0
 * @version 4.4.0
 */
define( 'Views/PostSearch',[], function() {

	return Backbone.View.extend( {

		/**
		 * DOM Events
		 *
		 * @type     obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		events: {
			'select2:select': 'add_post',
		},

		/**
		 * Wrapper Tag name
		 *
		 * @type  {String}
		 */
		tagName: 'select',

		/**
		 * Initializer
		 *
		 * @param    obj   data  customize the search box with data
		 * @return   void
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		initialize: function( data ) {

			this.post_type         = data.post_type;
			this.searching_message = data.searching_message || LLMS.l10n.translate( 'Searching...' );

		},

		/**
		 * Select event, adds the existing lesson to the course
		 *
		 * @param    obj   event  select2:select event object
		 * @since    3.16.0
		 * @version  3.17.0
		 */
		add_post: function( event ) {

			var type = this.$el.attr( 'data-post-type' );

			Backbone.pubSub.trigger( type.replace( 'llms_', '' ) + '-search-select', event.params.data, event );
			this.$el.val( null ).trigger( 'change' );

		},

		/**
		 * Render the section
		 *
		 * Initializes a new collection and views for all lessons in the section.
		 *
		 * @since 3.16.0
		 * @since 3.16.12 Unknown.
		 * @since 4.4.0 Update ajax nonce source.
		 *
		 * @return void
		 */
		render: function() {
			var self = this;
			setTimeout( function () {
				self.$el.llmsSelect2( {
					ajax: {
						dataType: 'JSON',
						delay: 250,
						method: 'POST',
						url: window.ajaxurl,
						data: function( params ) {
							return {
								action: 'llms_builder',
								action_type: 'search',
								course_id: window.llms_builder.course.id,
								post_type: self.post_type,
								term: params.term,
								page: params.page,
								_ajax_nonce: window.llms.ajax_nonce,
							};
						},
					},
					dropdownParent: $( '.wrap.lifterlms.llms-builder' ),
					// Don't escape html from render_result.
					escapeMarkup: function( markup ) {
						return markup;
					},
					placeholder: self.searching_message,
					templateResult: self.render_result,
					width: '100%',
				} );
				self.$el.attr( 'data-post-type', self.post_type );
			}, 0 );
			return this;

		},

		/**
		 * Render a nicer UI for each search result in the in the Select2 search results
		 *
		 * @param    object   res  result data
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.12
		 */
		render_result: function( res ) {

			var $html = $( '<div class="llms-existing-lesson-result" />' );

			if ( res.loading ) {
				return $html.append( res.text );
			}

			var $side = $( '<aside class="llms-existing-action" />' ),
				$main = $( '<div class="llms-existing-info" />' );
				icon  = ( 'attach' === res.action ) ? 'paperclip' : 'clone',
				text  = ( 'attach' === res.action ) ? LLMS.l10n.translate( 'Attach' ) : LLMS.l10n.translate( 'Clone' );

			$side.append( '<i class="fa fa-' + icon + '" aria-hidden="true"></i><small>' + text + '</small>' );

			$main.append( '<h4>' + res.data.title + '</h4>' );
			$main.append( '<h5>' + LLMS.l10n.translate( 'ID' ) + ': <em>' + res.data.id + '</em></h5>' );

			_.each( res.parents, function( parent ) {
				$main.append( '<h5>' + parent + '</em></h5>' );
			} );

			return $html.append( $side ).append( $main );

		},

	} );

} );

/**
 * Question Type View
 *
 * @since 3.16.0
 * @since 3.30.1 Fixed issue causing multiple binds for add_existing_question events.
 * @version 5.4.0
 */
define( 'Views/QuestionType',[ 'Views/Popover', 'Views/PostSearch' ], function( Popover, QuestionSearch ) {

	return Backbone.View.extend( {

		/**
		 * HTML class names.
		 *
		 * @type  {String}
		 */
		className: 'llms-question-type',

		events: {
			'click .llms-add-question': 'add_question',
		},

		/**
		 * HTML element wrapper ID attribute.
		 *
		 * @since 3.16.0
		 *
		 * @return {String}
		 */
		id: function() {
			return 'llms-question-type-' + this.model.id;
		},

		/**
		 * Wrapper Tag name.
		 *
		 * @type {String}
		 */
		tagName: 'li',

		/**
		 * Get the underscore template.
		 *
		 * @type {[type]}
		 */
		template: wp.template( 'llms-question-type-template' ),

		/**
		 * Initialization callback func (renders the element on screen).
		 *
		 * @since 3.16.0
		 *
		 * @return {Void}
		 */
		initialize: function() {

			this.render();

		},

		/**
		 * Compiles the template and renders the view.
		 *
		 * @since 3.16.0
		 *
		 * @return {Self} For chaining.
		 */
		render: function() {
			this.$el.html( this.template( this.model ) );
			return this;
		},

		/**
		 * Add a question of the selected type to the current quiz.
		 *
		 * @since 3.16.0
		 * @since 3.27.0 Unknown.
		 *
		 * @return {Void}
		 */
		add_question: function() {

			if ( 'existing' === this.model.get( 'id' ) ) {
				this.add_existing_question_click();
			} else {
				this.add_new_question();
			}

		},

		/**
		 * Add a new question to the quiz.
		 *
		 * @since 3.27.0
		 * @since 3.30.1 Fixed issue causing multiple binds.
		 *
		 * @return {Void}
		 */
		add_existing_question_click: function() {

			var pop = new Popover( {
				el: '#llms-add-question--existing',
				args: {
					backdrop: true,
					closeable: true,
					container: '#llms-builder-sidebar',
					dismissible: true,
					placement: 'top-left',
					width: 'calc( 100% - 40px )',
					offsetLeft: 250,
					offsetTop: 60,
					title: LLMS.l10n.translate( 'Add Existing Question' ),
					content: new QuestionSearch( {
						post_type: 'llms_question',
						searching_message: LLMS.l10n.translate( 'Search for existing questions...' ),
					} ).render().$el,
				}
			} );

			pop.show();
			Backbone.pubSub.on( 'question-search-select', this.add_existing_question, this );
			Backbone.pubSub.on( 'question-search-select', function( event ) {
				pop.hide();
				Backbone.pubSub.off( 'question-search-select', this.add_existing_question, this );
			}, this );

		},

		/**
		 * Callback event fired when a question is selected from the Add Existing Question popover interface.
		 *
		 * @since 3.27.0
		 * @since 5.4.0 Use author id instead of the question author object.
		 *
		 * @param {Object} event JS event object.
		 * @return {Void}
		 */
		add_existing_question: function( event ) {

			var question = event.data;

			if ( 'clone' === event.action ) {
				question = _.prepareQuestionObjectForCloning( question );
			} else {
				// Use author id instead of the question author object.
				question = _.prepareExistingPostObjectDataForAddingOrCloning( question );
				question._forceSync = true;
			}

			question._expanded = true;
			this.quiz.add_question( question );

			this.quiz.trigger( 'new-question-added' );

		},

		/**
		 * Add a new question to the quiz.
		 *
		 * @since 3.27.0
		 *
		 * @return {Void}
		 */
		add_new_question: function() {

			this.quiz.add_question( {
				_expanded: true,
				choices: this.model.get( 'default_choices' ) ? this.model.get( 'default_choices' ) : null,
				question_type: this.model,
			} );

			this.quiz.trigger( 'new-question-added' );

		},

		// filter: function( term ) {

		// var words = this.model.get_keywords().map( function( word ) {
		// return word.toLowerCase();
		// } );

		// term = term.toLowerCase();

		// if ( -1 === words.indexOf( term ) ) {
		// this.$el.addClass( 'filtered' );
		// } else {
		// this.$el.removeClass( 'filtered' );
		// }

		// },

		// clear_filter: function() {
		// this.$el.removeClass( 'filtered' );
		// }

	} );

} );

/**
 * Quiz question bank view
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/QuestionBank',[ 'Views/QuestionType' ], function( QuestionView ) {

	return Backbone.CollectionView.extend( {

		className: 'llms-question',

		/**
		 * Parent element
		 *
		 * @type  {String}
		 */
		el: '#llms-question-bank',

		/**
		 * Section model
		 *
		 * @type  {[type]}
		 */
		modelView: QuestionView,

		/**
		 * Are sections selectable?
		 *
		 * @type  {Bool}
		 */
		selectable: false,

		/**
		 * Are sections sortable?
		 *
		 * @type  {Bool}
		 */
		sortable: false,

	} );

} );

/**
 * Single Question Choice View
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/QuestionChoice',[ 'Views/_Editable', ], function( Editable ) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * HTML class names
		 * @type  {String}
		 */
		className: 'llms-question-choice',

		events: _.defaults( {
			'change input[name="correct"]': 'toggle_correct',
			'click .llms-action-icon[href="#llms-add-choice"]': 'add_choice',
			'click .llms-action-icon[href="#llms-del-choice"]': 'del_choice',
		}, Editable.events ),

		/**
		 * HTML element wrapper ID attribute
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		id: function() {
			return 'llms-question-choice-' + this.model.id;
		},

		/**
		 * Wrapper Tag name
		 * @type  {String}
		 */
		tagName: 'li',

		/**
		 * Get the underscore template
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-question-choice-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 * @return   void
		 * @since    3.14.1
		 * @version  3.14.1
		 */
		initialize: function() {

			this.render();

			this.listenTo( this.model.collection, 'add', this.maybe_disable_buttons );
			this.listenTo( this.model, 'change', this.render );

			if ( 'image' === this.model.get( 'choice_type' ) ) {
				this.listenTo( this.model.get( 'choice' ), 'change', this.render );
			}

		},

		/**
		 * Compiles the template and renders the view
		 * @return   self (for chaining)
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render: function() {
			this.$el.html( this.template( this.model ) );
			return this;
		},

		/**
		 * Add a new choice to the current choice list
		 * Adds *after* the clicked choice
		 * @param    obj   event  JS event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		add_choice: function( event ) {

			event.stopPropagation();
			event.preventDefault();

			var index = this.model.collection.indexOf( this.model );
			this.model.collection.parent.add_choice( {}, {
				at: index + 1,
			} );

		},

		/**
		 * Delete the choice from the choice list & ensure there's at least one correct choice
		 * @param    obj   event  js event obj
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		del_choice: function( event ) {

			event.preventDefault();
			Backbone.pubSub.trigger( 'model-trashed', this.model );
			this.model.collection.remove( this.model );

		},

		/**
		 * When the correct answer input changes sync status to model
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		toggle_correct: function() {

			var correct = this.$el.find( 'input[name="correct"]' ).is( ':checked' );
			this.model.set( 'correct', correct );
			this.model.collection.trigger( 'correct-update', this.model );

		},

	}, Editable ) );

} );

/**
 * Quiz question bank view
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/QuestionChoiceList',[ 'Views/QuestionChoice' ], function( ChoiceView ) {

	return Backbone.CollectionView.extend( {

		className: 'llms-quiz-questions',

		/**
		 * Choice model view
		 *
		 * @type  {[type]}
		 */
		modelView: ChoiceView,

		/**
		 * Enable keyboard events
		 *
		 * @type  {Bool}
		 */
		processKeyEvents: false,

		/**
		 * Are sections selectable?
		 *
		 * @type  {Bool}
		 */
		selectable: false,

		/**
		 * Are sections sortable?
		 *
		 * @type  {Bool}
		 */
		sortable: true,

		sortableOptions: {
			axis: false,
			// connectWith: '.llms-lessons',
			cursor: 'move',
			handle: '.llms-choice-id',
			items: '.llms-question-choice',
			placeholder: 'llms-question-choice llms-sortable-placeholder',
		},

		sortable_start: function( model ) {
			this.$el.addClass( 'dragging' );
		},

		sortable_stop: function( model ) {
			this.$el.removeClass( 'dragging' );
		},

	} );

} );

/**
 * Single Question View
 * @since    3.16.0
 * @version  3.27.0
 */
define( 'Views/Question',[
		'Views/_Detachable',
		'Views/_Editable',
		'Views/QuestionChoiceList'
	], function(
		Detachable,
		Editable,
		ChoiceListView
	) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * Generate CSS classes for the question
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		className: function() {
			return 'llms-question qtype--' + this.model.get( 'question_type' ).get( 'id' );
		},

		events: _.defaults( {
			'click .clone--question': 'clone',
			'click .delete--question': 'delete',
			'click .expand--question': 'expand',
			'click .collapse--question': 'collapse',
			'change input[name="question_points"]': 'update_points',
		}, Detachable.events, Editable.events ),

		/**
		 * HTML element wrapper ID attribute
		 * @return   string
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		id: function() {
			return 'llms-question-' + this.model.id;
		},

		/**
		 * Wrapper Tag name
		 * @type  {String}
		 */
		tagName: 'li',

		/**
		 * Get the underscore template
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-question-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function() {

			var change_events = [
				'change:_expanded',
				'change:menu_order',
			];
			_.each( change_events, function( event ) {
				this.listenTo( this.model, event, this.render );
			}, this );

			this.listenTo( this.model.get( 'image' ), 'change', this.render );

			this.listenTo( this.model.get_parent(), 'change:_points', this.render_points_percentage );

			this.on( 'multi_choices_toggle', this.multi_choices_toggle, this );

			Backbone.pubSub.on( 'del-question-choice', this.del_choice, this );

		},

		/**
		 * Compiles the template and renders the view
		 * @return   self (for chaining)
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render: function() {

			this.$el.html( this.template( this.model ) );

			if ( this.model.get( 'question_type').get( 'choices' ) ) {

				this.choiceListView = new ChoiceListView( {
					el: this.$el.find( '.llms-question-choices' ),
					collection: this.model.get( 'choices' ),
				} );
				this.choiceListView.render();
				this.choiceListView.on( 'sortStart', this.choiceListView.sortable_start );
				this.choiceListView.on( 'sortStop', this.choiceListView.sortable_stop );

			}

			if ( 'group' === this.model.get( 'question_type' ).get( 'id' ) ) {

				var self = this;
				setTimeout( function() {
					self.questionListView = self.collectionListView.quiz.get_question_list( {
						el: self.$el.find( '.llms-quiz-questions' ),
						collection: self.model.get( 'questions' ),
					} );
					self.questionListView.render();
					self.questionListView.on( 'sortStart', self.questionListView.sortable_start );
					self.questionListView.on( 'sortStop', self.questionListView.sortable_stop );
				}, 1 );

			}

			if ( this.model.get( 'description_enabled' ) ) {
				this.init_editor( 'question-desc--' + this.model.get( 'id' ) );
			}

			if ( this.model.get( 'clarifications_enabled' ) ) {
				this.init_editor( 'question-clarifications--' + this.model.get( 'id' ), {
					mediaButtons: false,
					tinymce: {
						toolbar1: 'bold,italic,strikethrough,bullist,numlist,alignleft,aligncenter,alignright',
						toolbar2: '',
						setup: _.bind( this.on_editor_ready, this ),
					}
				} );
			}

			this.init_formatting_els();
			this.init_selects();

			return this;
		},

		/**
		 * rerender points percentage when question points are updated
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render_points_percentage: function() {

			this.$el.find( '.llms-question-points' ).attr( 'data-tip', this.model.get_points_percentage() );

		},

		/**
		 * Click event to duplicate a question within a quiz
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		clone: function( event ) {

			event.stopPropagation();
			event.preventDefault();
			this.model.collection.add( this._get_question_clone( this.model ) );

		},

		/**
		 * Recursive clone function which will correctly clone children of a question
		 * @param    obj   question  question model
		 * @return   obj
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_get_question_clone: function( question ) {

			// create a duplicate
			var clone = _.clone( question.attributes );

			// remove id (we want the duplicate to have a temp id)
			delete clone.id;

			clone.parent_id = question.get( 'id' );

			// set the question type ID
			clone.question_type = question.get( 'question_type' ).get( 'id' );

			// clone the image attributes separately
			clone.image = _.clone( question.get( 'image' ).attributes );

			// if it has choices clone all the choices
			if ( question.get( 'choices' ) ) {

				clone.choices = [];

				question.get( 'choices' ).each( function ( choice ) {

					var choice_clone = _.clone( choice.attributes );
					delete choice_clone.id;
					delete choice_clone.question_id;

					clone.choices.push( choice_clone );

				} );

			}

			if ( 'group' === question.get( 'question_type' ).get( 'id' ) ) {

				clone.questions = [];
				question.get( 'questions' ).each( function( child ) {
					clone.questions.push( this._get_question_clone( child ) );
				}, this );

			}

			return clone;

		},

		/**
		 * Collapse a question and hide it's settings
		 * @param obj event js event obj.
		 * @return   void
		 * @since    3.16.0
		 * @version  3.27.0
		 */
		collapse: function( event ) {

			if ( event ) {
				event.preventDefault();
			}

			this.model.set( '_expanded', false );

		},

		/**
		 * Delete the question from a quiz / question group
		 * @param    obj   event  js event object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		delete: function( event ) {

			event.preventDefault();

			if ( window.confirm( LLMS.l10n.translate( 'Are you sure you want to delete this question?' ) ) ) {

				this.model.collection.remove( this.model );
				Backbone.pubSub.trigger( 'model-trashed', this.model );

			}

		},

		/**
		 * Click event to reveal a question's settings & choices
		 * @param obj event js event obj.
		 * @return   void
		 * @since    3.16.0
		 * @version  3.27.0
		 */
		expand: function( event ) {

			if ( event ) {
				event.preventDefault();
			}

			this.model.set( '_expanded', true );

		},

		/**
		 * When toggling multiple correct answers *off* remove all correct choices except the first correct choice in the list
		 * @param    string   val  value of the question's `multi_choice` attr [yes|no]
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		multi_choices_toggle: function( val ) {

			if ( 'yes' === val ) {
				return;
			}

			this.model.get( 'choices' ).update_correct( _.first( this.model.get( 'choices' ).get_correct() ) );

		},

		/**
		 * Update the model's points when the value of the points input is updated
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		update_points: function() {

			this.model.set( 'points', this.$el.find( 'input[name="question_points"]' ).val() * 1 );

		}

	}, Detachable, Editable ) );

} );

/**
 * Quiz question bank view
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/QuestionList',[ 'Views/Question' ], function( QuestionView ) {

	return Backbone.CollectionView.extend( {

		className: 'llms-quiz-questions',

		/**
		 * Parent element
		 * @type  {String}
		 */
		// el: '#llms-quiz-questions',

		/**
		 * Section model
		 * @type  {[type]}
		 */
		modelView: QuestionView,

		/**
		 * Enable keyboard events
		 * @type  {Bool}
		 */
		processKeyEvents: false,

		/**
		 * Are sections selectable?
		 * @type  {Bool}
		 */
		selectable: false,

		/**
		 * Are sections sortable?
		 * @type  {Bool}
		 */
		sortable: true,

		sortableOptions: {
			axis: false,
			connectWith: '.llms-quiz-questions',
			cursor: 'move',
			handle: '.llms-data-stamp',
			items: '.llms-question',
			placeholder: 'llms-question llms-sortable-placeholder',
		},

		/**
		 * Highlight drop areas when dragging starts
		 * @param    obj   model  model being sorted
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		sortable_start: function( model ) {
			var selector = 'group' === model.get( 'question_type' ).get( 'id' ) ? '.llms-editor-tab > .llms-quiz-questions' : '.llms-quiz-questions';
			$( selector ).addClass( 'dragging' );
		},

		/**
		 * Remove highlights when dragging stops
		 * @param    obj   model  model being sorted
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		sortable_stop: function() {
			$( '.llms-quiz-questions' ).removeClass( 'dragging' );
		},

		/**
		 * Overrides receive to ensure that question groups can't be moved into question groups
		 * @param    obj   event  js event object
		 * @param    obj   ui     jQuery UI Sortable ui object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_receive : function( event, ui ) {

			event.stopPropagation();

			// prevent moving a question group into a question group
			if ( ui.item.hasClass( 'qtype--group' ) && $( event.target ).closest( '.qtype--group' ).length ) {;
				ui.sender.sortable( 'cancel' );
				return;
			}

			var senderListEl = ui.sender;
			var senderCollectionListView = senderListEl.data( "view" );
			if( ! senderCollectionListView || ! senderCollectionListView.collection ) return;

			var newIndex = this._getContainerEl().children().index( ui.item );
			var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) );
			senderCollectionListView.collection.remove( modelReceived );
			this.collection.add( modelReceived, { at : newIndex } );
			modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value.
			this.setSelectedModel( modelReceived );
		},

		/**
		 * Override to allow manipulation of placeholder element
		 * @param    {[type]}   event  [description]
		 * @param    {[type]}   ui     [description]
		 * @return   {[type]}
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_sortStart : function( event, ui ) {

			var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) );

			ui.placeholder.addClass( 'qtype--' + modelBeingSorted.get( 'question_type' ).get( 'id' ) );

			if( this._isBackboneCourierAvailable() )
				this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } );
			else this.trigger( "sortStart", modelBeingSorted );
		},

		/**
		 * Overloads the function from Backbone.CollectionView core because it doesn't send stop events
		 * if moving from one sortable to another... :-(
		 * @param    obj   event  js event object
		 * @param    obj   ui     jQuery UI object
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		_sortStop : function( event, ui ) {

			event.stopPropagation();

			var modelBeingSorted = this.collection.get( ui.item.attr( 'data-model-cid' ) ),
				modelViewContainerEl = this._getContainerEl(),
				newIndex = modelViewContainerEl.children().index( ui.item );

			if ( newIndex == -1 && modelBeingSorted ) {
				this.collection.remove( modelBeingSorted );
			}

			this._reorderCollectionBasedOnHTML();
			this.updateDependentControls();

			if( this._isBackboneCourierAvailable() ) {
				this.spawn( 'sortStop', { modelBeingSorted : modelBeingSorted, newIndex : newIndex } );
			} else {
				this.trigger( 'sortStop', modelBeingSorted, newIndex );
			}

		},

	} );

} );

/**
 * Single Quiz View.
 *
 * @since 3.16.0
 * @version 5.4.0
 */
define( 'Views/Quiz',[
		'Models/Quiz',
		'Views/Popover',
		'Views/PostSearch',
		'Views/QuestionBank',
		'Views/QuestionList',
		'Views/SettingsFields',
		'Views/_Detachable',
		'Views/_Editable',
		'Views/_Subview',
		'Views/_Trashable'
	], function(
		QuizModel,
		Popover,
		PostSearch,
		QuestionBank,
		QuestionList,
		SettingsFields,
		Detachable,
		Editable,
		Subview,
		Trashable
	) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * Current view state.
		 *
		 * @type {String}
		 */
		state: 'default',

		/**
		 * Current Subviews.
		 *
		 * @type {Object}
		 */
		views: {
			settings: {
				class: SettingsFields,
				instance: null,
				state: 'default',
			},
			bank: {
				class: QuestionBank,
				instance: null,
				state: 'default',
			},
			list: {
				class: QuestionList,
				instance: null,
				state: 'default',
			},
		},

		el: '#llms-editor-quiz',

		/**
		 * Events.
		 *
		 * @type {Object}
		 */
		events: _.defaults( {
			'click #llms-existing-quiz': 'add_existing_quiz_click',
			'click #llms-new-quiz': 'add_new_quiz',
			'click #llms-show-question-bank': 'show_tools',
			'click .bulk-toggle': 'bulk_toggle',
			// 'keyup #llms-question-bank-filter': 'filter_question_types',
			// 'search #llms-question-bank-filter': 'filter_question_types',
		}, Detachable.events, Editable.events, Trashable.events ),

		/**
		 * Wrapper Tag name.
		 *
		 * @type {String}
		 */
		tagName: 'div',

		/**
		 * Get the underscore template
		 *
		 * @type {[type]}
		 */
		template: wp.template( 'llms-quiz-template' ),

		/**
		 * Initialization callback func (renders the element on screen).
		 *
		 * @since 3.16.0
		 * @since 3.19.2 Unknown.
		 *
		 * @return {Void}
		 */
		initialize: function( data ) {

			this.lesson = data.lesson;

			// Initialize the model if the quiz is enabled or it's disabled but we still have data for a quiz.
			if ( 'yes' === this.lesson.get( 'quiz_enabled' ) || ! _.isEmpty( this.lesson.get( 'quiz' ) ) ) {

				this.model = this.lesson.get( 'quiz' );

				/**
				 * @todo  this is a terrible terrible patch
				 *        I've spent nearly 3 days trying to figure out how to not use this line of code
				 *        ISSUE REPRODUCTION:
				 *        Open course builder
				 *        Open a lesson (A) and add a quiz
				 *        Switch to a new lesson (B)
				 *        Add a new quiz
				 *        Return to lesson A and the quizzes parent will be set to LESSON B
				 *        This will happen for *every* quiz in the builder...
				 *        Adding this set_parent on init guarantees that the quizzes correct parent is set
				 *        after adding new quizzes to other lessons
				 *        it's awful and it's gross...
				 *        I'm confused and tired and going to miss release dates again because of it
				 */
				this.model.set_parent( this.lesson );

				this.listenTo( this.model, 'change:_points', this.render_points );

			}

			this.on( 'model-trashed', this.on_trashed );

		},

		/**
		 * Compiles the template and renders the view.
		 *
		 * @since 3.16.0
		 * @since 3.19.2 Unknown.
		 *
		 * @return {Self} For chaining.
		 */
		render: function() {

			this.$el.html( this.template( this.model ) );

			// Render the quiz builder.
			if ( this.model ) {

				// Don't allow interaction until questions are lazy loaded.
				LLMS.Spinner.start( this.$el );

				this.render_subview( 'settings', {
					el: '#llms-quiz-settings-fields',
					model: this.model,
				} );

				this.init_datepickers();
				this.init_selects();

				this.render_subview( 'bank', {
					collection: window.llms_builder.questions,
				} );

				var last_group = null,
					group = null;
				// Let all the question types reference the quiz for adding questions quickly.
				this.get_subview( 'bank' ).instance.viewManager.each( function( view ) {

					view.quiz = this.model;

					group = view.model.get( 'group' ).name;

					if ( last_group !== group ) {
						last_group = group;
						view.$el.before( '<li class="llms-question-bank-header"><h4>' + group + '</h4></li>' );
					}

				}, this );

				this.model.load_questions( _.bind( function( err ) {

					if ( err ) {
						alert( LLMS.l10n.translate( 'An error occurred while trying to load the questions. Please refresh the page and try again.' ) );
						return this;
					}

					LLMS.Spinner.stop( this.$el );
					this.render_subview( 'list', {
						el: '#llms-quiz-questions',
						collection: this.model.get( 'questions' ),
					} );
					var list = this.get_subview( 'list' ).instance;
					list.quiz = this;
					list.collection.on( 'add', function() {
						list.collection.trigger( 'reorder' );
					}, this );
					list.on( 'sortStart', list.sortable_start );
					list.on( 'sortStop', list.sortable_stop );

				}, this ) );

				this.model.on( 'new-question-added', function() {
					var $questions = this.$el.find( '#llms-quiz-questions' );
					$questions.animate( { scrollTop: $questions.prop( 'scrollHeight' ) }, 200 );
				}, this );

			}

			return this;

		},

		/**
		 * On quiz points update, update the value of the Total Points area in the header.
		 *
		 * @since 3.17.6
		 *
		 * @param {Object} quiz   Instance of the quiz model.
		 * @param {Int}    points Updated number of points.
		 * @return {Void}
		 */
		render_points: function( quiz, points ) {

			this.$el.find( '#llms-quiz-total-points' ).text( points );

		},

		/**
		 * Bulk expand / collapse question buttons.
		 *
		 * @since 3.16.0
		 *
		 * @param {Object} Event JS event object.
		 * @return {Void}
		 */
		bulk_toggle: function( event ) {

			var expanded = ( 'expand' === $( event.target ).attr( 'data-action' ) );

			this.model.get( 'questions' ).each( function( question ) {
				question.set( '_expanded', expanded );
			} );

		},

		/**
		 * Adds a new quiz to a lesson which currently has no quiz associated with it.
		 *
		 * @since 3.16.0
		 *
		 * @return {Void}
		 */
		add_new_quiz: function() {

			var quiz = this.lesson.get( 'quiz' );
			if ( _.isEmpty( quiz ) ) {
				quiz = this.lesson.add_quiz();
			} else {
				this.lesson.set( 'quiz_enabled', 'yes' );
			}

			this.model = quiz;
			this.render();

		},


		/**
		 * Add an existing quiz to a lesson.
		 *
		 * @since 3.16.0
		 * @since 3.24.0 Unknown.
		 * @since 5.4.0 Use author id instead of the quiz author object.
		 *
		 * @param {Object} event JS event object.
		 * @return {Void}
		 */
		add_existing_quiz: function( event ) {

			this.post_search_popover.hide();

			var quiz = event.data;

			if ( 'clone' === event.action ) {

				quiz = _.prepareQuizObjectForCloning( quiz );

			} else {

				// Use author id instead of the quiz author object.
				quiz = _.prepareExistingPostObjectDataForAddingOrCloning( quiz );
				quiz._forceSync = true;

			}

			delete quiz.lesson_id;

			this.lesson.add_quiz( quiz );
			this.model = this.lesson.get( 'quiz' );
			this.render();

		},

		/**
		 * Open add existing quiz popover.
		 *
		 * @since 3.16.12
		 *
		 * @param {Object} event JS event object.
		 * @return {Void}
		 */
		add_existing_quiz_click: function( event ) {

			event.preventDefault();

			this.post_search_popover = new Popover( {
				el: '#llms-existing-quiz',
				args: {
					backdrop: true,
					closeable: true,
					container: '.wrap.lifterlms.llms-builder',
					dismissible: true,
					placement: 'left',
					width: 480,
					title: LLMS.l10n.translate( 'Add Existing Quiz' ),
					content: new PostSearch( {
						post_type: 'llms_quiz',
						searching_message: LLMS.l10n.translate( 'Search for existing quizzes...' ),
					} ).render().$el,
					onHide: function() {
						Backbone.pubSub.off( 'quiz-search-select' );
					},
				}
			} );

			this.post_search_popover.show();
			Backbone.pubSub.once( 'quiz-search-select', this.add_existing_quiz, this );

		},

		// filter_question_types: _.debounce( function( event ) {

		// 	var term = $( event.target ).val();

		// 	this.QuestionBankView.viewManager.each( function( view ) {
		// 		if ( ! term ) {
		// 			view.clear_filter();
		// 		} else {
		// 			view.filter( term );
		// 		}
		// 	} );


		// }, 300 ),

		/**
		 * Callback function when the quiz has been deleted.
		 *
		 * @since 3.16.6
		 *
		 * @param {Oject} quiz Quiz Model.
		 * @return {Void}
		 */
		on_trashed: function( quiz ) {

			this.lesson.set( 'quiz_enabled', 'no' );
			this.lesson.set( 'quiz', '' );

			delete this.model;

			this.render();

		},

		/**
		 * "Add Question" button click event.
		 *
		 * @since 3.16.0
		 *
		 * Creates a popover with question type list interface.
		 *
		 * @return {Void}
		 */
		show_tools: function() {

			// Create popover,
			var pop = new Popover( {
				el: '#llms-show-question-bank',
				args: {
					backdrop: true,
					closeable: true,
					container: '#llms-builder-sidebar',
					dismissible: true,
					placement: 'top-left',
					width: 'calc( 100% - 40px )',
					title: LLMS.l10n.translate( 'Add a Question' ),
					url: '#llms-quiz-tools',
				}
			} );

			// Show it.
			pop.show();

			// If a question is added, hide the popover.
			this.model.on( 'new-question-added', function() {
				pop.hide();
			} );

		},

		get_question_list: function( options ) {
			return new QuestionList( options );
		}

	}, Detachable, Editable, Subview, Trashable, SettingsFields ) );

} );

/**
 * Single Assignment View.
 *
 * @package LifterLMS/Scripts
 *
 * @since 3.17.0
 * @version 5.4.0
 */

define( 'Views/Assignment',[
		'Views/Popover',
		'Views/PostSearch',
		'Views/_Detachable',
		'Views/_Editable',
		'Views/_Trashable',
		'Views/_Subview',
		'Views/SettingsFields'
	], function(
		Popover,
		PostSearch,
		Detachable,
		Editable,
		Trashable,
		Subview,
		SettingsFields
	) {

		return Backbone.View.extend( _.defaults( {

			/**
			 * Current view state.
			 *
			 * @type {String}
			 */
			state: 'default',

			/**
			 * Current Subviews.
			 *
			 * @type {Object}
			 */
			views: {
				settings: {
					class: SettingsFields,
					instance: null,
					state: 'default',
				},
			},

			el: '#llms-editor-assignment',

			/**
			 * DOM Events.
			 *
			 * @since 3.17.1
			 *
			 * @return {Object}
			 */
			events: function() {
				var addon_events = this.is_addon_available() ? window.llms_builder.assignments.get_view_events() : {};
				return _.defaults( {
					'click #llms-existing-assignment': 'add_existing_assignment_click',
					'click #llms-new-assignment': 'add_new_assignment',
				}, Detachable.events, Editable.events, Trashable.events, addon_events );
			},

			/**
			 * Wrapper Tag name.
			 *
			 * @type {String}
			 */
			tagName: 'div',

			/**
			 * Get the underscore template.
			 *
			 * @type {[type]}
			 */
			template: wp.template( 'llms-assignment-template' ),

			/**
			 * Initialization callback func (renders the element on screen).
			 *
			 * @since 3.17.0
			 * @since 3.17.2 Unknown.
			 *
			 * @return {Void}
			 */
			initialize: function( data ) {

				this.lesson = data.lesson;

				// initialize the model if the assignment is enabled or it's disabled but we still have data for a assignment
				if ( 'yes' === this.lesson.get( 'assignment_enabled' ) || ! _.isEmpty( this.lesson.get( 'assignment' ) ) ) {

					this.model = this.lesson.get( 'assignment' );

					/**
					 * Todo Item.
					 *
					 * @todo  this is a terrible terrible patch
					 *        I've spent nearly 3 days trying to figure out how to not use this line of code
					 *        ISSUE REPRODUCTION:
					 *        Open course builder
					 *        Open a lesson (A) and add a assignment
					 *        Switch to a new lesson (B)
					 *        Add a new assignment
					 *        Return to lesson A and the assignment's parent will be set to LESSON B
					 *        This will happen for *every* assignment in the builder...
					 *        Adding this set_parent on init guarantees that the assignment's correct parent is set
					 *        after adding new assignment's to other lessons
					 *        it's awful and it's gross...
					 *        I'm confused and tired and going to miss release dates again because of it
					 */
					this.model.set_parent( this.lesson );

				}

				this.on( 'model-trashed', this.on_trashed );

			},

			/**
			 * Compiles the template and renders the view.
			 *
			 * @since 3.17.0
			 * @since 3.17.7 Unknown.
			 *
			 * @return {Self} For chaining.
			 */
			render: function() {

				this.$el.html( this.template( this.model ) );

				if ( this.model && this.is_addon_available() ) {

					this.stopListening( this.model, 'change:assignment_type', this.render );

					this.render_subview( 'settings', {
						el: '#llms-assignment-settings-fields',
						model: this.model,
					} );

					// this.init_datepickers();
					this.init_selects();

					window.llms_builder.assignments.render_editor( this );

					this.listenTo( this.model, 'change:assignment_type', this.render );

				}

				return this;

			},

			/**
			 * Adds a new assignment to a lesson which currently has no assignment associated with it.
			 *
			 * @since 3.17.0
			 *
			 * @return {Void}
			 */
			add_new_assignment: function() {

				if ( this.is_addon_available() ) {

					this.model = window.llms_builder.assignments.get_assignment( {
						/* Translators: %1$s = associated lesson title */
						title: LLMS.l10n.replace( '%1$s Assignment', {
							'%1$s': this.lesson.get( 'title' ),
						} ),
                        lesson_id: this.lesson.get( 'id' ),
					} );

					this.lesson.set( 'assignment_enabled', 'yes' );
					this.lesson.set( 'assignment', this.model );

					this.render();

				} else {

					this.show_ad_popover( '#llms-new-assignment' );

				}

			},

			/**
			 * When an assignment is selected from the post select popover
			 * instantiate it and add it to the current lesson.
			 *
			 * @param {Object} event Data from the select2 select event.
			 *
			 * @since 3.17.0
			 * @since 5.4.0 Prepare assignment object for cloning and use author id instead of the quiz author object.
			 */
			add_existing_assignment: function( event ) {

				this.post_search_popover.hide();

				var assignment = event.data;

				if ( 'clone' === event.action ) {

					assignment = _.prepareAssignmentObjectForCloning( assignment );

				} else {

					// Use author id instead of the assignment author object.
					assignment = _.prepareExistingPostObjectDataForAddingOrCloning( assignment );
					assignment._forceSync = true;

				}

				assignment.lesson_id = this.lesson.get( 'id' )

				assignment = window.llms_builder.construct.get_model( 'Assignment', assignment );

				this.lesson.set( 'assignment_enabled', 'yes' );
				this.lesson.set( 'assignment', assignment );
				this.model = assignment;

				this.render();

			},

			/**
			 * Open add existing assignment popover.
			 *
			 * @since 3.17.0
			 *
			 * @param {Object} event JS event object.
			 * @return {Void}
			 */
			add_existing_assignment_click: function( event ) {

				event.preventDefault();

				if ( this.is_addon_available() ) {

					this.post_search_popover = new Popover( {
						el: '#llms-existing-assignment',
						args: {
							backdrop: true,
							closeable: true,
							container: '.wrap.lifterlms.llms-builder',
							dismissible: true,
							placement: 'left',
							width: 480,
							title: LLMS.l10n.translate( 'Add Existing Assignment' ),
							content: new PostSearch( {
								post_type: 'llms_assignment',
								searching_message: LLMS.l10n.translate( 'Search for existing assignments...' ),
							} ).render().$el,
						onHide: function() {
							Backbone.pubSub.off( 'assignment-search-select' );
						},
						}
					} );

					this.post_search_popover.show();
					Backbone.pubSub.once( 'assignment-search-select', this.add_existing_assignment, this );

				} else {

					this.show_ad_popover( '#llms-existing-assignment' );

				}

			},

			/**
			 * Determine if Assignments addon is available to use.
			 *
			 * @since 3.17.0
			 *
			 * @return {Boolean}
			 */
			is_addon_available: function() {

				return ( window.llms_builder.assignments );

			},

			/**
			 * Called when assignment is trashed.
			 *
			 * @since 3.17.0
			 *
			 * @param {Oject} assignment Assignment Model.
			 * @return {Void}
			 */
			on_trashed: function( assignment ) {

				this.lesson.set( 'assignment_enabled', 'no' );
				this.lesson.set( 'assignment', '' );

				delete this.model;

				this.render();

			},

			/**
			 * Shows a dirty dirty ad popover for advanced assignments.
			 *
			 * @since 3.17.0
			 *
			 * @param {Sring} el The jQuery selector string.
			 * @return {Void}
			 */
			show_ad_popover: function( el ) {

				var h3 = LLMS.l10n.translate( 'Get Your Students Taking Action' ),
				p      = 'Great learning content is only half of teaching online. When your learners fully engage, they will take your content and move into action. Remove barriers for your learners by telling them what to do to apply what they just learned. Create graded assignments or simply give them a checklist of action items to complete before moving on.',
				btn    = LLMS.l10n.translate( 'Get Assignments Now!' ),
				url    = 'https://lifterlms.com/product/lifterlms-assignments?utm_source=LifterLMS%20Plugin&utm_medium=Assignment%20Builder%20Button&utm_campaign=Assignment%20Addon%20Upsell&utm_content=3.17.0';

				this.ad_popover = new Popover( {
					el: el,
					args: {
						backdrop: true,
						closeable: true,
						container: '.wrap.lifterlms.llms-builder',
						dismissible: true,
						// placement: 'left',
						width: 380,
						title: LLMS.l10n.translate( 'Unlock LifterLMS Assignments' ),
						content: '<h3>' + h3 + '</h3><p>' + p + '</p><br><p><a class="llms-button-primary" href="' + url + '" target="_blank">' + btn + '</a></p>'
					}
				} );

				this.ad_popover.show();

			},

		}, Detachable, Editable, Trashable, Subview, SettingsFields ) );

	} );

/**
 * Sidebar Editor View
 *
 * @since    3.16.0
 * @version  3.27.0
 */
define( 'Views/Editor',[
		'Views/LessonEditor',
		'Views/Quiz',
		'Views/Assignment',
		'Views/_Subview'
	], function(
		LessonEditor,
		Quiz,
		Assignment,
		Subview
	) {

		return Backbone.View.extend( _.defaults( {

			/**
			 * Current view state
			 *
			 * @type  {String}
			 */
			state: 'lesson', // [lesson|quiz]

			/**
			 * Current Subviews
			 *
			 * @type  {Object}
			 */
			views: {
				lesson: {
					class: LessonEditor,
					instance: null,
					state: 'lesson',
				},
				assignment: {
					class: Assignment,
					instance: null,
					state: 'assignment',
				},
				quiz: {
					class: Quiz,
					instance: null,
					state: 'quiz',
				},
			},

			/**
			 * HTML element selector
			 *
			 * @type  {String}
			 */
			el: '#llms-editor',

			events: {
				'click .llms-editor-nav a[href="#llms-editor-close"]': 'close_editor',
				'click .llms-editor-nav a:not([href="#llms-editor-close"])': 'switch_tab',
			},

			/**
			 * Wrapper Tag name
			 *
			 * @type  {String}
			 */
			tagName: 'div',

			/**
			 * Get the underscore template
			 *
			 * @type  {[type]}
			 */
			template: wp.template( 'llms-editor-template' ),

			/**
			 * Initialization callback func (renders the element on screen)
			 *
			 * @return   void
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			initialize: function( data ) {

				this.SidebarView = data.SidebarView;
				if ( data.tab ) {
					this.state = data.tab;
				}

			},

			/**
			 * Compiles the template and renders the view
			 *
			 * @return   self (for chaining)
			 * @since    3.16.0
			 * @version  3.16.0
			 */
			render: function( view_data ) {

				view_data = view_data || {};

				this.$el.html( this.template( this ) );

				this.render_subviews( _.extend( view_data, {
					lesson: this.model,
				} ) );

				return this;

			},

			/**
			 * Click event for close sidebar editor button
			 * Sends event to main SidebarView to trigger editor closing events
			 *
			 * @param    obj   event  js event obj
			 * @return   void
			 * @since    3.16.0
			 * @version  3.27.0
			 */
			close_editor: function( event ) {

				event.preventDefault();
				Backbone.pubSub.trigger( 'sidebar-editor-close' );
				window.location.hash = '';

			},

			/**
			 * Click event for switching tabs in the editor navigation
			 *
			 * @param    object  event  js event object
			 * @return   void
			 * @since    3.16.0
			 * @version  3.27.0
			 */
			switch_tab: function( event ) {

				event.preventDefault();

				var $btn = $( event.target ),
				view     = $btn.attr( 'data-view' ),
				$tab     = this.$el.find( $btn.attr( 'href' ) );

				this.set_state( view ).render();
				this.set_hash( view );

				// Backbone.pubSub.trigger( 'editor-tab-activated', $btn.attr( 'href' ).substring( 1 ) );
			},

			/**
			 * Adds a hash for deep linking to a specific lesson tab
			 *
			 * @param  string  subtab subtab [quiz|assignment]
			 * @return void
			 * @since   3.27.0
			 * @version 3.27.0
			 */
			set_hash: function( subtab ) {

				var hash = 'lesson:' + this.model.get( 'id' );

				if ( 'lesson' !== subtab ) {
					hash += ':' + subtab;
				}

				window.location.hash = hash;

			},

		}, Subview ) );

} );

/**
 * Sidebar Elements View
 *
 * @since    3.16.0
 * @version  3.16.12
 */
define( 'Views/Elements',[ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'Views/Popover', 'Views/PostSearch' ], function( Section, SectionView, Lesson, LessonView, Popover, LessonSearch ) {

	return Backbone.View.extend( {

		/**
		 * HTML element selector
		 *
		 * @type  {String}
		 */
		el: '#llms-elements',

		events: {
			'click #llms-new-section': 'add_new_section',
			'click #llms-new-lesson': 'add_new_lesson',
			'click #llms-existing-lesson': 'add_existing_lesson',
		},

		/**
		 * Wrapper Tag name
		 *
		 * @type  {String}
		 */
		tagName: 'div',

		/**
		 * Get the underscore template
		 *
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-elements-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function( data ) {

			// save a reference to the main Course view
			this.SidebarView = data.SidebarView;

			// watch course sections and enable/disable lesson buttons conditionally
			this.listenTo( this.SidebarView.CourseView.model.get( 'sections' ), 'add', this.maybe_disable_buttons );
			this.listenTo( this.SidebarView.CourseView.model.get( 'sections' ), 'remove', this.maybe_disable_buttons );

		},

		/**
		 * Compiles the template and renders the view
		 *
		 * @return   self (for chaining)
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render: function() {

			this.$el.html( this.template() );
			this.draggable();
			this.maybe_disable_buttons();

			return this;
		},

		draggable: function() {

			$( '#llms-new-section' ).draggable( {
				appendTo: '#llms-sections',
				cancel: false,
				connectToSortable: '.llms-sections',
				helper: function() {
					return new SectionView( { model: new Section() } ).render().$el;
				},
				start: function() {
					$( '.llms-sections' ).addClass( 'dragging' );
				},
				stop: function() {
					$( '.llms-sections' ).removeClass( 'dragging' );
				},
			} );

			$( '#llms-new-lesson' ).draggable( {
				// appendTo: '#llms-sections .llms-section:first-child .llms-lessons',
				appendTo: '#llms-sections',
				cancel: false,
				connectToSortable: '.llms-lessons',
				helper: function() {
					return new LessonView( { model: new Lesson() } ).render().$el;
				},
				start: function() {

					$( '.llms-lessons' ).addClass( 'dragging' );

				},
				stop: function() {
					$( '.llms-lessons' ).removeClass( 'dragging' );
					$( '.drag-expanded' ).removeClass( '.drag-expanded' );
				},
			} );

		},

		add_new_section: function( event ) {

			event.preventDefault();
			Backbone.pubSub.trigger( 'add-new-section' );
		},

		add_new_lesson: function( event ) {
			event.preventDefault();
			Backbone.pubSub.trigger( 'add-new-lesson' );
		},

		/**
		 * Show the popover to add an existing lessons
		 *
		 * @param    object   event  JS Event Object
		 * @return   void
		 * @since    3.16.12
		 * @version  3.16.12
		 */
		add_existing_lesson: function( event ) {

			event.preventDefault();

			var pop = new Popover( {
				el: '#llms-existing-lesson',
				args: {
					backdrop: true,
					closeable: true,
					container: '.wrap.lifterlms.llms-builder',
					dismissible: true,
					placement: 'left',
					width: 480,
					title: LLMS.l10n.translate( 'Add Existing Lesson' ),
					content: new LessonSearch( {
						post_type: 'lesson',
						searching_message: LLMS.l10n.translate( 'Search for existing lessons...' ),
					} ).render().$el,
				}
			} );

			pop.show();
			Backbone.pubSub.on( 'lesson-search-select', function() {
				pop.hide()
			} );

		},

		/**
		 * Disables lesson add buttons if no sections are available to add a lesson to
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		maybe_disable_buttons: function() {

			var $els = $( '#llms-new-lesson, #llms-existing-lesson' );

			if ( ! this.SidebarView.CourseView.model.get( 'sections' ).length ) {
				$els.attr( 'disabled', 'disabled' );
			} else {
				$els.removeAttr( 'disabled' );
			}

		},

	} );

} );

/**
 * Sidebar Utilities View
 *
 * @since    3.16.0
 * @version  3.16.0
 */
define( 'Views/Utilities',[], function() {

	return Backbone.View.extend( {

		/**
		 * HTML element selector
		 *
		 * @type  {String}
		 */
		el: '#llms-utilities',

		events: {
			'click #llms-collapse-all': 'collapse_all',
			'click #llms-expand-all': 'expand_all'
		},

		/**
		 * Wrapper Tag name
		 *
		 * @type  {String}
		 */
		tagName: 'div',

		/**
		 * Get the underscore template
		 *
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-utilities-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function() {

			// this.render();
		},

		/**
		 * Compiles the template and renders the view
		 *
		 * @return   self (for chaining)
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render: function() {
			this.$el.html( this.template() );
			return this;
		},

		/**
		 * Collapse all sections
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		collapse_all: function( event ) {
			event.preventDefault();
			Backbone.pubSub.trigger( 'collapse-all' );
		},

		/**
		 * Expand all sections
		 *
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		expand_all: function( event ) {
			event.preventDefault();
			Backbone.pubSub.trigger( 'expand-all' );
		},

	} );

} );

/**
 * Main sidebar view
 * @since    3.16.0
 * @version  3.16.7
 */
define( 'Views/Sidebar',[
		'Views/Editor',
		'Views/Elements',
		'Views/Utilities',
		'Views/_Subview'
	], function(
		Editor,
		Elements,
		Utilities,
		Subview
	) {

	return Backbone.View.extend( _.defaults( {

		/**
		 * Current builder state
		 * @type  {String}
		 */
		state: 'builder', // [builder|editor]

		/**
		 * Current Subviews
		 * @type  {Object}
		 */
		views: {
			elements: {
				class: Elements,
				instance: null,
				state: 'builder',
			},
			utilities: {
				class: Utilities,
				instance: null,
				state: 'builder',
			},
			editor: {
				class: Editor,
				instance: null,
				state: 'editor',
			},
		},

		/**
		 * HTML element selector
		 * @type  {String}
		 */
		el: '#llms-builder-sidebar',

		/**
		 * DOM events
		 * @type  {Object}
		 */
		events: {
			'click #llms-save-button': 'save_now',
			'click #llms-exit-button': 'exit_now',
			'click .llms-builder-error': 'clear_errors',
		},

		/**
		 * Wrapper Tag name
		 * @type  {String}
		 */
		tagName: 'aside',

		/**
		 * Get the underscore template
		 * @type  {[type]}
		 */
		template: wp.template( 'llms-sidebar-template' ),

		/**
		 * Initialization callback func (renders the element on screen)
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		initialize: function( data ) {

			// save a reference to the main Course view
			this.CourseView = data.CourseView;

			this.render();

			Backbone.pubSub.on( 'current-save-status', this.changes_made, this );

			Backbone.pubSub.on( 'heartbeat-send', this.heartbeat_send, this );
			Backbone.pubSub.on( 'heartbeat-tick', this.heartbeat_tick, this );

			Backbone.pubSub.on( 'lesson-selected', this.on_lesson_select, this );
			Backbone.pubSub.on( 'sidebar-editor-close', this.on_editor_close, this );

			this.$saveButton = $( '#llms-save-button' );

		},

		/**
		 * Compiles the template and renders the view
		 * @return   self (for chaining)
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		render: function( view_data ) {

			view_data = view_data || {};

			this.$el.html( this.template() );

			this.render_subviews( _.extend( view_data, {
				SidebarView: this,
			} ) );

			var $el = $( '.wrap.lifterlms.llms-builder' );
			if ( 'builder' === this.state ) {
				$el.removeClass( 'editor-active' );
			} else {
				$el.addClass( 'editor-active' );
			}

			this.$saveButton = this.$el.find( '#llms-save-button' );

			return this;

		},

		/**
		 * Adds error message element
		 * @param    {[type]}   $err  [description]
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		add_error: function( $err ) {

			this.$el.find( '.llms-builder-save' ).prepend( $err );

		},

		/**
		 * Clear any existing error message elements
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		clear_errors: function() {

			this.$el.find( '.llms-builder-save .llms-builder-error' ).remove();

		},

		/**
		 * Update save status button when changes are detected
		 * runs on an interval to check status of course regularly for unsaved changes
		 * @param    obj   sync  instance of the sync controller
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		changes_made: function( sync ) {

			// if a save is currently running, don't do anything
			if ( sync.saving ) {
				return;
			}

			if ( sync.has_unsaved_changes ) {

				this.$saveButton.attr( 'data-status', 'unsaved' );
				this.$saveButton.removeAttr( 'disabled' );

			} else {

				this.$saveButton.attr( 'data-status', 'saved' );
				this.$saveButton.attr( 'disabled', 'disabled' );

			}

		},

		/**
		 * Exit the builder and return to the WP Course Editor
		 * @return   void
		 * @since    3.16.7
		 * @version  3.16.7
		 */
		exit_now: function() {

			window.location.href = window.llms_builder.CourseModel.get_edit_post_link();

		},

		/**
		 * Triggered when a heartbeat send event starts containing builder information
		 * @param    obj   sync  instance of the sync controller
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		heartbeat_send: function( sync ) {

			if ( sync.saving ) {
				LLMS.Spinner.start( this.$saveButton.find( 'i' ), 'small' );
				this.$saveButton.attr( {
					'data-status': 'saving',
					disabled: 'disabled',
				} );
			}

		},

		/**
		 * Triggered when a heartbeat tick completes and updates save status or appends errors
		 * @param    obj   sync  instance of the sync controller
		 * @param    obj   data  updated data
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		heartbeat_tick: function( sync, data ) {

			if ( ! sync.saving ) {

				var status = 'saved';

				this.clear_errors();

				if ( 'error' === data.status ) {

					status = 'error';

					var msg = data.message,
						$err = $( '<ol class="llms-builder-error" />' );

					if ( 'object' === typeof msg ) {
						_.each( msg, function( txt ) {
							$err.append( '<li>' + txt + '</li>' );
						} );
					} else {
						$err = $err.append( '<li>' + msg + '</li>' );;
					}

					this.add_error( $err );

				}

				this.$saveButton.find( '.llms-spinning' ).remove();
				this.$saveButton.attr( {
					'data-status': status,
					disabled: 'disabled',
				} );

			}

		},

		/**
		 * Determine if the editor is the currently active state
		 * @return   boolean
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		is_editor_active: function() {

			return ( 'editor' === this.state );

		},

		/**
		 * Triggered when the editor closes, updates state to be the course builder view
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_editor_close: function() {

			this.set_state( 'builder' ).render();

		},

		/**
		 * When a lesson is selected, opens the sidebar to the editor view
		 * @param    obj   lesson_model  instance of the lesson model which was selected
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		on_lesson_select: function( lesson_model, tab ) {

			if ( 'editor' !== this.state ) {
				this.set_state( 'editor' );
			} else {
				this.remove_subview( 'editor' );
			}

			this.render( {
				model: lesson_model,
				tab: tab,
			} );

		},

		/**
		 * Save button click event
		 * @return   void
		 * @since    3.16.0
		 * @version  3.16.0
		 */
		save_now: function() {

			window.llms_builder.sync.save_now();

		},

	}, Subview ) );

} );

/**
 * LifterLMS JS Builder App Bootstrap
 *
 * @since 3.16.0
 * @since 3.37.11 Added `_.getEditor()` helper.
 * @version 5.4.0
 */
require( [
	'vendor/wp-hooks',
	'vendor/backbone.collectionView',
	'vendor/backbone.trackit',
	'Controllers/Construct',
	'Controllers/Debug',
	'Controllers/Schemas',
	'Controllers/Sync',
	'Models/loader',
	'Views/Course',
	'Views/Sidebar'
	], function(
	Hooks,
	CV,
	TrackIt,
	Construct,
	Debug,
	Schemas,
	Sync,
	Models,
	CourseView,
	SidebarView
	) {

		window.llms_builder.debug     = new Debug( window.llms_builder.debug );
		window.llms_builder.construct = new Construct();
		window.llms_builder.schemas   = new Schemas( window.llms_builder.schemas );

		/**
		 * Compare values, used by _.checked & _.selected mixins.
		 *
		 * @since 3.17.2
		 *
		 * @param {Mixed} expected expected Value, probably a string (the value of a select option or checkbox element).
		 * @param {Mixed} mixed    actual   Actual value, probably a string (the return of model.get( 'something' ) )
		 *                                  but could be an array like a multiselect.
		 * @return {Bool}
		 */
		function value_compare( expected, actual ) {
			return ( ( _.isArray( actual ) && -1 !== actual.indexOf( expected ) ) || expected == actual );
		};

		/**
		 * Underscores templating utilities
		 *
		 * @since    3.17.0
		 * @version  3.27.0
		 */
		_.mixin( {

			/**
			 * Determine if two values are equal and output checked attribute if they are.
			 *
			 * Useful for templating checkboxes & radio elements
			 * like WP Core PHP checked() but in JS.
			 *
			 * @since 3.17.0
			 * @since 3.17.2 Unknown.
			 *
			 * @param {Mixed} expected Expected element value.
			 * @param {Mixed} actual   Actual element value.
			 * @return {String}
			 */
			checked: function( expected, actual ) {
				if ( value_compare( expected, actual ) ) {
					return ' checked="checked"';
				}
				return '';
			},

			/**
			 * Recursively clone an object via _.clone().
			 *
			 * @since 3.17.7
			 *
			 * @param {Object} obj Object to clone.
			 * @return {Object}
			 */
			deepClone: function( obj ) {

				var clone = _.clone( obj );

				_.each( clone, function( val, key ) {
					if ( ! _.isFunction( val ) && _.isObject( val ) ) {
						clone[ key ] = _.deepClone( val );
					};
				} );

				return clone;

			},

			/**
			 * Retrieve the wp.editor instance.
			 *
			 * Uses `wp.oldEditor` (when available) which was added in WordPress 5.0.
			 *
			 * Falls back to `wp.editor()` which will usually be the same as `wp.oldEditor` unless
			 * the `@wordpress/editor` module has been loaded by another plugin or a theme.
			 *
			 * @since 3.37.11
			 *
			 * @return {Object}
			 */
			getEditor: function() {

				if ( undefined !== wp.oldEditor ) {

					var ed = wp.oldEditor;

					// Inline scripts added by WordPress are not ported to `wp.oldEditor`, see https://github.com/WordPress/WordPress/blob/641c632b0c9fde4e094b217f50749984ca43a2fa/wp-includes/class-wp-editor.php#L977.
					if ( undefined !== wp.editor && undefined !== wp.editor.getDefaultSettings ) {
						ed.getDefaultSettings = wp.editor.getDefaultSettings;
					}

					return ed;

				} else if ( undefined !== wp.editor && undefined !== wp.editor.autop ){

					return wp.editor;

				}

			},

			/**
			 * Strips IDs & Parent References from quizzes and all quiz questions.
			 *
			 * @since 3.24.0
			 * @since 3.27.0 Unknown.
			 * @since 5.4.0 Use author id instead of the question author object.
			 *
			 * @param {Object} quiz Raw quiz object (not a model).
			 * @return {Object}
			 */
			prepareQuizObjectForCloning: function( quiz ) {

				delete quiz.id;
				delete quiz.lesson_id;

				_.each( quiz.questions, function( question ) {

					question = _.prepareQuestionObjectForCloning( question );

				} );

				// Use author id instead of the quiz author object.
				quiz = _.prepareExistingPostObjectDataForAddingOrCloning( quiz );

				return quiz;

			},

			/**
			 * Strips IDs & Parent References from a question.
			 *
			 * @since 3.27.0
			 * @since 5.4.0 Use author id instead of the question author object.
			 *
			 * @param {Object} question Raw question object (not a model).
			 * @return {Object}
			 */
			prepareQuestionObjectForCloning: function( question ) {

				delete question.id;
				delete question.parent_id;

				if ( question.image && _.isObject( question.image ) ) {
					question.image._forceSync = true;
				}

				if ( question.choices ) {

					_.each( question.choices, function( choice ) {

						delete choice.question_id;
						delete choice.id;
						if ( 'image' === choice.choice_type && _.isObject( choice.choice ) ) {
							choice.choice._forceSync = true;
						}

					} );

				}

				// Use author id instead of the question author object.
				question = _.prepareExistingPostObjectDataForAddingOrCloning( question );

				return question;

			},

			/**
			 * Strips IDs & Parent References from assignments and all assignment tasks.
			 *
			 * @since 5.4.0
			 *
			 * @param {Object} assignment Raw assignment object (not a model).
			 * @return {Object}
			 */
			 prepareAssignmentObjectForCloning: function( assignment ) {

				delete assignment.id;
				delete assignment.lesson_id;

				// Clone tasks.
				if ( 'tasklist' === assignment.assignment_type ) {
					_.each( assignment.tasks, function( task ) {
						delete task.id;
						delete task.assignment_id;
					} );
				}

				// Use author id instead of the quiz author object.
				assignment = _.prepareExistingPostObjectDataForAddingOrCloning( assignment );

				return assignment;

			},

			/**
			 * Prepare post object data for adding or cloning.
			 *
			 * Use author id instead of the post type author object.
			 *
			 * @since 5.4.0
			 *
			 * @param {Object} quiz Raw post object (not a model).
			 * @return {Object}
			 */
			prepareExistingPostObjectDataForAddingOrCloning: function( post_data ) {

				if ( post_data.author && _.isObject( post_data.author ) && post_data.author.id ) {
					post_data.author = post_data.author.id;
				}

				return post_data;

			},

			/**
			 * Determine if two values are equal and output selected attribute if they are.
			 *
			 * Useful for templating select elements
			 * like WP Core PHP selected() but in JS.
			 *
			 *
			 * @since 3.17.0
			 * @since 3.17.2 Unknown.
			 *
			 * @param {Mixed} expected Expected element value.
			 * @param {Mixed} actual   Actual element value.
			 * @return {String}
			 */
			selected: function( expected, actual ) {
				if ( value_compare( expected, actual ) ) {
					return ' selected="selected"';
				}
				return '';
			},

			/**
			 * Generic function for stripping HTML tags from a string.
			 *
			 * @since 3.17.8
			 *
			 * @param {String} content      Raw string.
			 * @param {Array}  allowed_tags Array of allowed HTML tags.
			 * @return {String}
			 */
			stripFormatting: function( content, allowed_tags ) {

				if ( ! allowed_tags ) {
					allowed_tags = [ 'b', 'i', 'u', 'strong', 'em' ];
				}

				var $html = $( '<div>' + content + '</div>' );

				$html.find( '*' ).not( allowed_tags.join( ',' ) ).each( function( ) {

					$( this ).replaceWith( this.innerHTML );

				} );

				return $html.html();

			},

		} );

		Backbone.pubSub = _.extend( {}, Backbone.Events );

		$( document ).trigger( 'llms-builder-pre-init' );

		window.llms_builder.questions = window.llms_builder.construct.get_collection( 'QuestionTypes', window.llms_builder.questions );

		var CourseModel                 = window.llms_builder.construct.get_model( 'Course', window.llms_builder.course );
		window.llms_builder.CourseModel = CourseModel;

		window.llms_builder.sync = new Sync( CourseModel, window.llms_builder.sync );

		var Course = new CourseView( {
			model: CourseModel,
		} );

		var Sidebar = new SidebarView( {
			CourseView: Course
		} );

		$( document ).trigger( 'llms-builder-init', {
			course: Course,
			sidebar: Sidebar,
		} );

		/**
		 * Do deep linking to Lesson / Quiz / Assignments.
		 *
		 * Hash should be in the form of #lesson:{lesson_id}:{subtab}
		 * subtab can be either "quiz" or "assignment". If none found assumes the "lesson" tab.
		 *
		 * @since 3.27.0
		 * @since 3.30.1 Wait for wp.editor & window.tinymce to load before opening deep link tabs.
		 * @since 3.37.11 Use `_.getEditor()` helper when checking for the presence of `wp.editor`.
		 */
		if ( window.location.hash ) {

			var hash = window.location.hash;
			if ( -1 === hash.indexOf( '#lesson:' ) ) {
				return;
			}
			var parts = hash.replace( '#lesson:', '' ).split( ':' ),
			$lesson   = $( '#llms-lesson-' + parts[0] );

			if ( $lesson.length ) {

				LLMS.wait_for( function() {
					return ( undefined !== _.getEditor() && undefined !== window.tinymce );
                    }, function() {
					$lesson.closest( '.llms-builder-item.llms-section' ).find( 'a.llms-action-icon.expand' ).trigger( 'click' );
					var subtab = parts[1] ? parts[1] : 'lesson';
					$( '#llms-lesson-' + parts[0] ).find( 'a.llms-action-icon.edit-' + subtab ).trigger( 'click' );
				} );

			}

		}

	} );

define("main", function(){});

}(jQuery));

//# sourceMappingURL=../maps/js/llms-builder.js.map