wp/wp-includes/js/wp-api.js
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     1 (function( window, undefined ) {
       
     2 
       
     3 	'use strict';
       
     4 
       
     5 	/**
       
     6 	 * Initialise the WP_API.
       
     7 	 */
       
     8 	function WP_API() {
       
     9 		/** @namespace wp.api.models */
       
    10 		this.models = {};
       
    11 		/** @namespace wp.api.collections */
       
    12 		this.collections = {};
       
    13 		/** @namespace wp.api.views */
       
    14 		this.views = {};
       
    15 	}
       
    16 
       
    17 	/** @namespace wp */
       
    18 	window.wp            = window.wp || {};
       
    19 	/** @namespace wp.api */
       
    20 	wp.api               = wp.api || new WP_API();
       
    21 	wp.api.versionString = wp.api.versionString || 'wp/v2/';
       
    22 
       
    23 	// Alias _includes to _.contains, ensuring it is available if lodash is used.
       
    24 	if ( ! _.isFunction( _.includes ) && _.isFunction( _.contains ) ) {
       
    25 	  _.includes = _.contains;
       
    26 	}
       
    27 
       
    28 })( window );
       
    29 
       
    30 (function( window, undefined ) {
       
    31 
       
    32 	'use strict';
       
    33 
       
    34 	var pad, r;
       
    35 
       
    36 	/** @namespace wp */
       
    37 	window.wp = window.wp || {};
       
    38 	/** @namespace wp.api */
       
    39 	wp.api = wp.api || {};
       
    40 	/** @namespace wp.api.utils */
       
    41 	wp.api.utils = wp.api.utils || {};
       
    42 
       
    43 	/**
       
    44 	 * Determine model based on API route.
       
    45 	 *
       
    46 	 * @param {string} route    The API route.
       
    47 	 *
       
    48 	 * @return {Backbone Model} The model found at given route. Undefined if not found.
       
    49 	 */
       
    50 	wp.api.getModelByRoute = function( route ) {
       
    51 		return _.find( wp.api.models, function( model ) {
       
    52 			return model.prototype.route && route === model.prototype.route.index;
       
    53 		} );
       
    54 	};
       
    55 
       
    56 	/**
       
    57 	 * Determine collection based on API route.
       
    58 	 *
       
    59 	 * @param {string} route    The API route.
       
    60 	 *
       
    61 	 * @return {Backbone Model} The collection found at given route. Undefined if not found.
       
    62 	 */
       
    63 	wp.api.getCollectionByRoute = function( route ) {
       
    64 		return _.find( wp.api.collections, function( collection ) {
       
    65 			return collection.prototype.route && route === collection.prototype.route.index;
       
    66 		} );
       
    67 	};
       
    68 
       
    69 
       
    70 	/**
       
    71 	 * ECMAScript 5 shim, adapted from MDN.
       
    72 	 * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
       
    73 	 */
       
    74 	if ( ! Date.prototype.toISOString ) {
       
    75 		pad = function( number ) {
       
    76 			r = String( number );
       
    77 			if ( 1 === r.length ) {
       
    78 				r = '0' + r;
       
    79 			}
       
    80 
       
    81 			return r;
       
    82 		};
       
    83 
       
    84 		Date.prototype.toISOString = function() {
       
    85 			return this.getUTCFullYear() +
       
    86 				'-' + pad( this.getUTCMonth() + 1 ) +
       
    87 				'-' + pad( this.getUTCDate() ) +
       
    88 				'T' + pad( this.getUTCHours() ) +
       
    89 				':' + pad( this.getUTCMinutes() ) +
       
    90 				':' + pad( this.getUTCSeconds() ) +
       
    91 				'.' + String( ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ) ).slice( 2, 5 ) +
       
    92 				'Z';
       
    93 		};
       
    94 	}
       
    95 
       
    96 	/**
       
    97 	 * Parse date into ISO8601 format.
       
    98 	 *
       
    99 	 * @param {Date} date.
       
   100 	 */
       
   101 	wp.api.utils.parseISO8601 = function( date ) {
       
   102 		var timestamp, struct, i, k,
       
   103 			minutesOffset = 0,
       
   104 			numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ];
       
   105 
       
   106 		// ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string
       
   107 		// before falling back to any implementation-specific date parsing, so that’s what we do, even if native
       
   108 		// implementations could be faster.
       
   109 		//              1 YYYY                2 MM       3 DD           4 HH    5 mm       6 ss        7 msec        8 Z 9 ±    10 tzHH    11 tzmm
       
   110 		if ( ( struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec( date ) ) ) {
       
   111 
       
   112 			// Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC.
       
   113 			for ( i = 0; ( k = numericKeys[i] ); ++i ) {
       
   114 				struct[k] = +struct[k] || 0;
       
   115 			}
       
   116 
       
   117 			// Allow undefined days and months.
       
   118 			struct[2] = ( +struct[2] || 1 ) - 1;
       
   119 			struct[3] = +struct[3] || 1;
       
   120 
       
   121 			if ( 'Z' !== struct[8]  && undefined !== struct[9] ) {
       
   122 				minutesOffset = struct[10] * 60 + struct[11];
       
   123 
       
   124 				if ( '+' === struct[9] ) {
       
   125 					minutesOffset = 0 - minutesOffset;
       
   126 				}
       
   127 			}
       
   128 
       
   129 			timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] );
       
   130 		} else {
       
   131 			timestamp = Date.parse ? Date.parse( date ) : NaN;
       
   132 		}
       
   133 
       
   134 		return timestamp;
       
   135 	};
       
   136 
       
   137 	/**
       
   138 	 * Helper function for getting the root URL.
       
   139 	 * @return {[type]} [description]
       
   140 	 */
       
   141 	wp.api.utils.getRootUrl = function() {
       
   142 		return window.location.origin ?
       
   143 			window.location.origin + '/' :
       
   144 			window.location.protocol + '/' + window.location.host + '/';
       
   145 	};
       
   146 
       
   147 	/**
       
   148 	 * Helper for capitalizing strings.
       
   149 	 */
       
   150 	wp.api.utils.capitalize = function( str ) {
       
   151 		if ( _.isUndefined( str ) ) {
       
   152 			return str;
       
   153 		}
       
   154 		return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
       
   155 	};
       
   156 
       
   157 	/**
       
   158 	 * Helper function that capitalizes the first word and camel cases any words starting
       
   159 	 * after dashes, removing the dashes.
       
   160 	 */
       
   161 	wp.api.utils.capitalizeAndCamelCaseDashes = function( str ) {
       
   162 		if ( _.isUndefined( str ) ) {
       
   163 			return str;
       
   164 		}
       
   165 		str = wp.api.utils.capitalize( str );
       
   166 
       
   167 		return wp.api.utils.camelCaseDashes( str );
       
   168 	};
       
   169 
       
   170 	/**
       
   171 	 * Helper function to camel case the letter after dashes, removing the dashes.
       
   172 	 */
       
   173 	wp.api.utils.camelCaseDashes = function( str ) {
       
   174 		return str.replace( /-([a-z])/g, function( g ) {
       
   175 			return g[ 1 ].toUpperCase();
       
   176 		} );
       
   177 	};
       
   178 
       
   179 	/**
       
   180 	 * Extract a route part based on negative index.
       
   181 	 *
       
   182 	 * @param {string}   route          The endpoint route.
       
   183 	 * @param {int}      part           The number of parts from the end of the route to retrieve. Default 1.
       
   184 	 *                                  Example route `/a/b/c`: part 1 is `c`, part 2 is `b`, part 3 is `a`.
       
   185 	 * @param {string}  [versionString] Version string, defaults to `wp.api.versionString`.
       
   186 	 * @param {boolean} [reverse]       Whether to reverse the order when extracting the route part. Optional, default false.
       
   187 	 */
       
   188 	wp.api.utils.extractRoutePart = function( route, part, versionString, reverse ) {
       
   189 		var routeParts;
       
   190 
       
   191 		part = part || 1;
       
   192 		versionString = versionString || wp.api.versionString;
       
   193 
       
   194 		// Remove versions string from route to avoid returning it.
       
   195 		if ( 0 === route.indexOf( '/' + versionString ) ) {
       
   196 			route = route.substr( versionString.length + 1 );
       
   197 		}
       
   198 
       
   199 		routeParts = route.split( '/' );
       
   200 		if ( reverse ) {
       
   201 			routeParts = routeParts.reverse();
       
   202 		}
       
   203 		if ( _.isUndefined( routeParts[ --part ] ) ) {
       
   204 			return '';
       
   205 		}
       
   206 		return routeParts[ part ];
       
   207 	};
       
   208 
       
   209 	/**
       
   210 	 * Extract a parent name from a passed route.
       
   211 	 *
       
   212 	 * @param {string} route The route to extract a name from.
       
   213 	 */
       
   214 	wp.api.utils.extractParentName = function( route ) {
       
   215 		var name,
       
   216 			lastSlash = route.lastIndexOf( '_id>[\\d]+)/' );
       
   217 
       
   218 		if ( lastSlash < 0 ) {
       
   219 			return '';
       
   220 		}
       
   221 		name = route.substr( 0, lastSlash - 1 );
       
   222 		name = name.split( '/' );
       
   223 		name.pop();
       
   224 		name = name.pop();
       
   225 		return name;
       
   226 	};
       
   227 
       
   228 	/**
       
   229 	 * Add args and options to a model prototype from a route's endpoints.
       
   230 	 *
       
   231 	 * @param {array}  routeEndpoints Array of route endpoints.
       
   232 	 * @param {Object} modelInstance  An instance of the model (or collection)
       
   233 	 *                                to add the args to.
       
   234 	 */
       
   235 	wp.api.utils.decorateFromRoute = function( routeEndpoints, modelInstance ) {
       
   236 
       
   237 		/**
       
   238 		 * Build the args based on route endpoint data.
       
   239 		 */
       
   240 		_.each( routeEndpoints, function( routeEndpoint ) {
       
   241 
       
   242 			// Add post and edit endpoints as model args.
       
   243 			if ( _.includes( routeEndpoint.methods, 'POST' ) || _.includes( routeEndpoint.methods, 'PUT' ) ) {
       
   244 
       
   245 				// Add any non empty args, merging them into the args object.
       
   246 				if ( ! _.isEmpty( routeEndpoint.args ) ) {
       
   247 
       
   248 					// Set as default if no args yet.
       
   249 					if ( _.isEmpty( modelInstance.prototype.args ) ) {
       
   250 						modelInstance.prototype.args = routeEndpoint.args;
       
   251 					} else {
       
   252 
       
   253 						// We already have args, merge these new args in.
       
   254 						modelInstance.prototype.args = _.extend( modelInstance.prototype.args, routeEndpoint.args );
       
   255 					}
       
   256 				}
       
   257 			} else {
       
   258 
       
   259 				// Add GET method as model options.
       
   260 				if ( _.includes( routeEndpoint.methods, 'GET' ) ) {
       
   261 
       
   262 					// Add any non empty args, merging them into the defaults object.
       
   263 					if ( ! _.isEmpty( routeEndpoint.args ) ) {
       
   264 
       
   265 						// Set as default if no defaults yet.
       
   266 						if ( _.isEmpty( modelInstance.prototype.options ) ) {
       
   267 							modelInstance.prototype.options = routeEndpoint.args;
       
   268 						} else {
       
   269 
       
   270 							// We already have options, merge these new args in.
       
   271 							modelInstance.prototype.options = _.extend( modelInstance.prototype.options, routeEndpoint.args );
       
   272 						}
       
   273 					}
       
   274 
       
   275 				}
       
   276 			}
       
   277 
       
   278 		} );
       
   279 
       
   280 	};
       
   281 
       
   282 	/**
       
   283 	 * Add mixins and helpers to models depending on their defaults.
       
   284 	 *
       
   285 	 * @param {Backbone Model} model          The model to attach helpers and mixins to.
       
   286 	 * @param {string}         modelClassName The classname of the constructed model.
       
   287 	 * @param {Object} 	       loadingObjects An object containing the models and collections we are building.
       
   288 	 */
       
   289 	wp.api.utils.addMixinsAndHelpers = function( model, modelClassName, loadingObjects ) {
       
   290 
       
   291 		var hasDate = false,
       
   292 
       
   293 			/**
       
   294 			 * Array of parseable dates.
       
   295 			 *
       
   296 			 * @type {string[]}.
       
   297 			 */
       
   298 			parseableDates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ],
       
   299 
       
   300 			/**
       
   301 			 * Mixin for all content that is time stamped.
       
   302 			 *
       
   303 			 * This mixin converts between mysql timestamps and JavaScript Dates when syncing a model
       
   304 			 * to or from the server. For example, a date stored as `2015-12-27T21:22:24` on the server
       
   305 			 * gets expanded to `Sun Dec 27 2015 14:22:24 GMT-0700 (MST)` when the model is fetched.
       
   306 			 *
       
   307 			 * @type {{toJSON: toJSON, parse: parse}}.
       
   308 			 */
       
   309 			TimeStampedMixin = {
       
   310 
       
   311 				/**
       
   312 				 * Prepare a JavaScript Date for transmitting to the server.
       
   313 				 *
       
   314 				 * This helper function accepts a field and Date object. It converts the passed Date
       
   315 				 * to an ISO string and sets that on the model field.
       
   316 				 *
       
   317 				 * @param {Date}   date   A JavaScript date object. WordPress expects dates in UTC.
       
   318 				 * @param {string} field  The date field to set. One of 'date', 'date_gmt', 'date_modified'
       
   319 				 *                        or 'date_modified_gmt'. Optional, defaults to 'date'.
       
   320 				 */
       
   321 				setDate: function( date, field ) {
       
   322 					var theField = field || 'date';
       
   323 
       
   324 					// Don't alter non parsable date fields.
       
   325 					if ( _.indexOf( parseableDates, theField ) < 0 ) {
       
   326 						return false;
       
   327 					}
       
   328 
       
   329 					this.set( theField, date.toISOString() );
       
   330 				},
       
   331 
       
   332 				/**
       
   333 				 * Get a JavaScript Date from the passed field.
       
   334 				 *
       
   335 				 * WordPress returns 'date' and 'date_modified' in the timezone of the server as well as
       
   336 				 * UTC dates as 'date_gmt' and 'date_modified_gmt'. Draft posts do not include UTC dates.
       
   337 				 *
       
   338 				 * @param {string} field  The date field to set. One of 'date', 'date_gmt', 'date_modified'
       
   339 				 *                        or 'date_modified_gmt'. Optional, defaults to 'date'.
       
   340 				 */
       
   341 				getDate: function( field ) {
       
   342 					var theField   = field || 'date',
       
   343 						theISODate = this.get( theField );
       
   344 
       
   345 					// Only get date fields and non null values.
       
   346 					if ( _.indexOf( parseableDates, theField ) < 0 || _.isNull( theISODate ) ) {
       
   347 						return false;
       
   348 					}
       
   349 
       
   350 					return new Date( wp.api.utils.parseISO8601( theISODate ) );
       
   351 				}
       
   352 			},
       
   353 
       
   354 			/**
       
   355 			 * Build a helper function to retrieve related model.
       
   356 			 *
       
   357 			 * @param  {string} parentModel      The parent model.
       
   358 			 * @param  {int}    modelId          The model ID if the object to request
       
   359 			 * @param  {string} modelName        The model name to use when constructing the model.
       
   360 			 * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
       
   361 			 * @param  {string} embedCheckField  Which model field to check to see if the model has data.
       
   362 			 *
       
   363 			 * @return {Deferred.promise}        A promise which resolves to the constructed model.
       
   364 			 */
       
   365 			buildModelGetter = function( parentModel, modelId, modelName, embedSourcePoint, embedCheckField ) {
       
   366 				var getModel, embeddeds, attributes, deferred;
       
   367 
       
   368 				deferred  = jQuery.Deferred();
       
   369 				embeddeds = parentModel.get( '_embedded' ) || {};
       
   370 
       
   371 				// Verify that we have a valid object id.
       
   372 				if ( ! _.isNumber( modelId ) || 0 === modelId ) {
       
   373 					deferred.reject();
       
   374 					return deferred;
       
   375 				}
       
   376 
       
   377 				// If we have embedded object data, use that when constructing the getModel.
       
   378 				if ( embeddeds[ embedSourcePoint ] ) {
       
   379 					attributes = _.findWhere( embeddeds[ embedSourcePoint ], { id: modelId } );
       
   380 				}
       
   381 
       
   382 				// Otherwise use the modelId.
       
   383 				if ( ! attributes ) {
       
   384 					attributes = { id: modelId };
       
   385 				}
       
   386 
       
   387 				// Create the new getModel model.
       
   388 				getModel = new wp.api.models[ modelName ]( attributes );
       
   389 
       
   390 				if ( ! getModel.get( embedCheckField ) ) {
       
   391 					getModel.fetch( {
       
   392 						success: function( getModel ) {
       
   393 							deferred.resolve( getModel );
       
   394 						},
       
   395 						error: function( getModel, response ) {
       
   396 							deferred.reject( response );
       
   397 						}
       
   398 					} );
       
   399 				} else {
       
   400 					// Resolve with the embedded model.
       
   401 					deferred.resolve( getModel );
       
   402 				}
       
   403 
       
   404 				// Return a promise.
       
   405 				return deferred.promise();
       
   406 			},
       
   407 
       
   408 			/**
       
   409 			 * Build a helper to retrieve a collection.
       
   410 			 *
       
   411 			 * @param  {string} parentModel      The parent model.
       
   412 			 * @param  {string} collectionName   The name to use when constructing the collection.
       
   413 			 * @param  {string} embedSourcePoint Where to check the embedds object for _embed data.
       
   414 			 * @param  {string} embedIndex       An addiitonal optional index for the _embed data.
       
   415 			 *
       
   416 			 * @return {Deferred.promise}        A promise which resolves to the constructed collection.
       
   417 			 */
       
   418 			buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) {
       
   419 				/**
       
   420 				 * Returns a promise that resolves to the requested collection
       
   421 				 *
       
   422 				 * Uses the embedded data if available, otherwises fetches the
       
   423 				 * data from the server.
       
   424 				 *
       
   425 				 * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ]
       
   426 				 * collection.
       
   427 				 */
       
   428 				var postId, embeddeds, getObjects,
       
   429 					classProperties = '',
       
   430 					properties      = '',
       
   431 					deferred        = jQuery.Deferred();
       
   432 
       
   433 				postId    = parentModel.get( 'id' );
       
   434 				embeddeds = parentModel.get( '_embedded' ) || {};
       
   435 
       
   436 				// Verify that we have a valid post id.
       
   437 				if ( ! _.isNumber( postId ) || 0 === postId ) {
       
   438 					deferred.reject();
       
   439 					return deferred;
       
   440 				}
       
   441 
       
   442 				// If we have embedded getObjects data, use that when constructing the getObjects.
       
   443 				if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) {
       
   444 
       
   445 					// Some embeds also include an index offset, check for that.
       
   446 					if ( _.isUndefined( embedIndex ) ) {
       
   447 
       
   448 						// Use the embed source point directly.
       
   449 						properties = embeddeds[ embedSourcePoint ];
       
   450 					} else {
       
   451 
       
   452 						// Add the index to the embed source point.
       
   453 						properties = embeddeds[ embedSourcePoint ][ embedIndex ];
       
   454 					}
       
   455 				} else {
       
   456 
       
   457 					// Otherwise use the postId.
       
   458 					classProperties = { parent: postId };
       
   459 				}
       
   460 
       
   461 				// Create the new getObjects collection.
       
   462 				getObjects = new wp.api.collections[ collectionName ]( properties, classProperties );
       
   463 
       
   464 				// If we didn’t have embedded getObjects, fetch the getObjects data.
       
   465 				if ( _.isUndefined( getObjects.models[0] ) ) {
       
   466 					getObjects.fetch( {
       
   467 						success: function( getObjects ) {
       
   468 
       
   469 							// Add a helper 'parent_post' attribute onto the model.
       
   470 							setHelperParentPost( getObjects, postId );
       
   471 							deferred.resolve( getObjects );
       
   472 						},
       
   473 						error: function( getModel, response ) {
       
   474 							deferred.reject( response );
       
   475 						}
       
   476 					} );
       
   477 				} else {
       
   478 
       
   479 					// Add a helper 'parent_post' attribute onto the model.
       
   480 					setHelperParentPost( getObjects, postId );
       
   481 					deferred.resolve( getObjects );
       
   482 				}
       
   483 
       
   484 				// Return a promise.
       
   485 				return deferred.promise();
       
   486 
       
   487 			},
       
   488 
       
   489 			/**
       
   490 			 * Set the model post parent.
       
   491 			 */
       
   492 			setHelperParentPost = function( collection, postId ) {
       
   493 
       
   494 				// Attach post_parent id to the collection.
       
   495 				_.each( collection.models, function( model ) {
       
   496 					model.set( 'parent_post', postId );
       
   497 				} );
       
   498 			},
       
   499 
       
   500 			/**
       
   501 			 * Add a helper function to handle post Meta.
       
   502 			 */
       
   503 			MetaMixin = {
       
   504 
       
   505 				/**
       
   506 				 * Get meta by key for a post.
       
   507 				 *
       
   508 				 * @param {string} key The meta key.
       
   509 				 *
       
   510 				 * @return {object} The post meta value.
       
   511 				 */
       
   512 				getMeta: function( key ) {
       
   513 					var metas = this.get( 'meta' );
       
   514 					return metas[ key ];
       
   515 				},
       
   516 
       
   517 				/**
       
   518 				 * Get all meta key/values for a post.
       
   519 				 *
       
   520 				 * @return {object} The post metas, as a key value pair object.
       
   521 				 */
       
   522 				getMetas: function() {
       
   523 					return this.get( 'meta' );
       
   524 				},
       
   525 
       
   526 				/**
       
   527 				 * Set a group of meta key/values for a post.
       
   528 				 *
       
   529 				 * @param {object} meta The post meta to set, as key/value pairs.
       
   530 				 */
       
   531 				setMetas: function( meta ) {
       
   532 					var metas = this.get( 'meta' );
       
   533 					_.extend( metas, meta );
       
   534 					this.set( 'meta', metas );
       
   535 				},
       
   536 
       
   537 				/**
       
   538 				 * Set a single meta value for a post, by key.
       
   539 				 *
       
   540 				 * @param {string} key   The meta key.
       
   541 				 * @param {object} value The meta value.
       
   542 				 */
       
   543 				setMeta: function( key, value ) {
       
   544 					var metas = this.get( 'meta' );
       
   545 					metas[ key ] = value;
       
   546 					this.set( 'meta', metas );
       
   547 				}
       
   548 			},
       
   549 
       
   550 			/**
       
   551 			 * Add a helper function to handle post Revisions.
       
   552 			 */
       
   553 			RevisionsMixin = {
       
   554 				getRevisions: function() {
       
   555 					return buildCollectionGetter( this, 'PostRevisions' );
       
   556 				}
       
   557 			},
       
   558 
       
   559 			/**
       
   560 			 * Add a helper function to handle post Tags.
       
   561 			 */
       
   562 			TagsMixin = {
       
   563 
       
   564 				/**
       
   565 				 * Get the tags for a post.
       
   566 				 *
       
   567 				 * @return {Deferred.promise} promise Resolves to an array of tags.
       
   568 				 */
       
   569 				getTags: function() {
       
   570 					var tagIds = this.get( 'tags' ),
       
   571 						tags  = new wp.api.collections.Tags();
       
   572 
       
   573 					// Resolve with an empty array if no tags.
       
   574 					if ( _.isEmpty( tagIds ) ) {
       
   575 						return jQuery.Deferred().resolve( [] );
       
   576 					}
       
   577 
       
   578 					return tags.fetch( { data: { include: tagIds } } );
       
   579 				},
       
   580 
       
   581 				/**
       
   582 				 * Set the tags for a post.
       
   583 				 *
       
   584 				 * Accepts an array of tag slugs, or a Tags collection.
       
   585 				 *
       
   586 				 * @param {array|Backbone.Collection} tags The tags to set on the post.
       
   587 				 *
       
   588 				 */
       
   589 				setTags: function( tags ) {
       
   590 					var allTags, newTag,
       
   591 						self = this,
       
   592 						newTags = [];
       
   593 
       
   594 					if ( _.isString( tags ) ) {
       
   595 						return false;
       
   596 					}
       
   597 
       
   598 					// If this is an array of slugs, build a collection.
       
   599 					if ( _.isArray( tags ) ) {
       
   600 
       
   601 						// Get all the tags.
       
   602 						allTags = new wp.api.collections.Tags();
       
   603 						allTags.fetch( {
       
   604 							data:    { per_page: 100 },
       
   605 							success: function( alltags ) {
       
   606 
       
   607 								// Find the passed tags and set them up.
       
   608 								_.each( tags, function( tag ) {
       
   609 									newTag = new wp.api.models.Tag( alltags.findWhere( { slug: tag } ) );
       
   610 
       
   611 									// Tie the new tag to the post.
       
   612 									newTag.set( 'parent_post', self.get( 'id' ) );
       
   613 
       
   614 									// Add the new tag to the collection.
       
   615 									newTags.push( newTag );
       
   616 								} );
       
   617 								tags = new wp.api.collections.Tags( newTags );
       
   618 								self.setTagsWithCollection( tags );
       
   619 							}
       
   620 						} );
       
   621 
       
   622 					} else {
       
   623 						this.setTagsWithCollection( tags );
       
   624 					}
       
   625 				},
       
   626 
       
   627 				/**
       
   628 				 * Set the tags for a post.
       
   629 				 *
       
   630 				 * Accepts a Tags collection.
       
   631 				 *
       
   632 				 * @param {array|Backbone.Collection} tags The tags to set on the post.
       
   633 				 *
       
   634 				 */
       
   635 				setTagsWithCollection: function( tags ) {
       
   636 
       
   637 					// Pluck out the category ids.
       
   638 					this.set( 'tags', tags.pluck( 'id' ) );
       
   639 					return this.save();
       
   640 				}
       
   641 			},
       
   642 
       
   643 			/**
       
   644 			 * Add a helper function to handle post Categories.
       
   645 			 */
       
   646 			CategoriesMixin = {
       
   647 
       
   648 				/**
       
   649 				 * Get a the categories for a post.
       
   650 				 *
       
   651 				 * @return {Deferred.promise} promise Resolves to an array of categories.
       
   652 				 */
       
   653 				getCategories: function() {
       
   654 					var categoryIds = this.get( 'categories' ),
       
   655 						categories  = new wp.api.collections.Categories();
       
   656 
       
   657 					// Resolve with an empty array if no categories.
       
   658 					if ( _.isEmpty( categoryIds ) ) {
       
   659 						return jQuery.Deferred().resolve( [] );
       
   660 					}
       
   661 
       
   662 					return categories.fetch( { data: { include: categoryIds } } );
       
   663 				},
       
   664 
       
   665 				/**
       
   666 				 * Set the categories for a post.
       
   667 				 *
       
   668 				 * Accepts an array of category slugs, or a Categories collection.
       
   669 				 *
       
   670 				 * @param {array|Backbone.Collection} categories The categories to set on the post.
       
   671 				 *
       
   672 				 */
       
   673 				setCategories: function( categories ) {
       
   674 					var allCategories, newCategory,
       
   675 						self = this,
       
   676 						newCategories = [];
       
   677 
       
   678 					if ( _.isString( categories ) ) {
       
   679 						return false;
       
   680 					}
       
   681 
       
   682 					// If this is an array of slugs, build a collection.
       
   683 					if ( _.isArray( categories ) ) {
       
   684 
       
   685 						// Get all the categories.
       
   686 						allCategories = new wp.api.collections.Categories();
       
   687 						allCategories.fetch( {
       
   688 							data:    { per_page: 100 },
       
   689 							success: function( allcats ) {
       
   690 
       
   691 								// Find the passed categories and set them up.
       
   692 								_.each( categories, function( category ) {
       
   693 									newCategory = new wp.api.models.Category( allcats.findWhere( { slug: category } ) );
       
   694 
       
   695 									// Tie the new category to the post.
       
   696 									newCategory.set( 'parent_post', self.get( 'id' ) );
       
   697 
       
   698 									// Add the new category to the collection.
       
   699 									newCategories.push( newCategory );
       
   700 								} );
       
   701 								categories = new wp.api.collections.Categories( newCategories );
       
   702 								self.setCategoriesWithCollection( categories );
       
   703 							}
       
   704 						} );
       
   705 
       
   706 					} else {
       
   707 						this.setCategoriesWithCollection( categories );
       
   708 					}
       
   709 
       
   710 				},
       
   711 
       
   712 				/**
       
   713 				 * Set the categories for a post.
       
   714 				 *
       
   715 				 * Accepts Categories collection.
       
   716 				 *
       
   717 				 * @param {array|Backbone.Collection} categories The categories to set on the post.
       
   718 				 *
       
   719 				 */
       
   720 				setCategoriesWithCollection: function( categories ) {
       
   721 
       
   722 					// Pluck out the category ids.
       
   723 					this.set( 'categories', categories.pluck( 'id' ) );
       
   724 					return this.save();
       
   725 				}
       
   726 			},
       
   727 
       
   728 			/**
       
   729 			 * Add a helper function to retrieve the author user model.
       
   730 			 */
       
   731 			AuthorMixin = {
       
   732 				getAuthorUser: function() {
       
   733 					return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' );
       
   734 				}
       
   735 			},
       
   736 
       
   737 			/**
       
   738 			 * Add a helper function to retrieve the featured media.
       
   739 			 */
       
   740 			FeaturedMediaMixin = {
       
   741 				getFeaturedMedia: function() {
       
   742 					return buildModelGetter( this, this.get( 'featured_media' ), 'Media', 'wp:featuredmedia', 'source_url' );
       
   743 				}
       
   744 			};
       
   745 
       
   746 		// Exit if we don't have valid model defaults.
       
   747 		if ( _.isUndefined( model.prototype.args ) ) {
       
   748 			return model;
       
   749 		}
       
   750 
       
   751 		// Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin.
       
   752 		_.each( parseableDates, function( theDateKey ) {
       
   753 			if ( ! _.isUndefined( model.prototype.args[ theDateKey ] ) ) {
       
   754 				hasDate = true;
       
   755 			}
       
   756 		} );
       
   757 
       
   758 		// Add the TimeStampedMixin for models that contain a date field.
       
   759 		if ( hasDate ) {
       
   760 			model = model.extend( TimeStampedMixin );
       
   761 		}
       
   762 
       
   763 		// Add the AuthorMixin for models that contain an author.
       
   764 		if ( ! _.isUndefined( model.prototype.args.author ) ) {
       
   765 			model = model.extend( AuthorMixin );
       
   766 		}
       
   767 
       
   768 		// Add the FeaturedMediaMixin for models that contain a featured_media.
       
   769 		if ( ! _.isUndefined( model.prototype.args.featured_media ) ) {
       
   770 			model = model.extend( FeaturedMediaMixin );
       
   771 		}
       
   772 
       
   773 		// Add the CategoriesMixin for models that support categories collections.
       
   774 		if ( ! _.isUndefined( model.prototype.args.categories ) ) {
       
   775 			model = model.extend( CategoriesMixin );
       
   776 		}
       
   777 
       
   778 		// Add the MetaMixin for models that support meta.
       
   779 		if ( ! _.isUndefined( model.prototype.args.meta ) ) {
       
   780 			model = model.extend( MetaMixin );
       
   781 		}
       
   782 
       
   783 		// Add the TagsMixin for models that support tags collections.
       
   784 		if ( ! _.isUndefined( model.prototype.args.tags ) ) {
       
   785 			model = model.extend( TagsMixin );
       
   786 		}
       
   787 
       
   788 		// Add the RevisionsMixin for models that support revisions collections.
       
   789 		if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) {
       
   790 			model = model.extend( RevisionsMixin );
       
   791 		}
       
   792 
       
   793 		return model;
       
   794 	};
       
   795 
       
   796 })( window );
       
   797 
       
   798 /* global wpApiSettings:false */
       
   799 
       
   800 // Suppress warning about parse function's unused "options" argument:
       
   801 /* jshint unused:false */
       
   802 (function() {
       
   803 
       
   804 	'use strict';
       
   805 
       
   806 	var wpApiSettings = window.wpApiSettings || {},
       
   807 	trashableTypes    = [ 'Comment', 'Media', 'Comment', 'Post', 'Page', 'Status', 'Taxonomy', 'Type' ];
       
   808 
       
   809 	/**
       
   810 	 * Backbone base model for all models.
       
   811 	 */
       
   812 	wp.api.WPApiBaseModel = Backbone.Model.extend(
       
   813 		/** @lends WPApiBaseModel.prototype  */
       
   814 		{
       
   815 
       
   816 			// Initialize the model.
       
   817 			initialize: function() {
       
   818 
       
   819 				/**
       
   820 				* Types that don't support trashing require passing ?force=true to delete.
       
   821 				*
       
   822 				*/
       
   823 				if ( -1 === _.indexOf( trashableTypes, this.name ) ) {
       
   824 					this.requireForceForDelete = true;
       
   825 				}
       
   826 			},
       
   827 
       
   828 			/**
       
   829 			 * Set nonce header before every Backbone sync.
       
   830 			 *
       
   831 			 * @param {string} method.
       
   832 			 * @param {Backbone.Model} model.
       
   833 			 * @param {{beforeSend}, *} options.
       
   834 			 * @returns {*}.
       
   835 			 */
       
   836 			sync: function( method, model, options ) {
       
   837 				var beforeSend;
       
   838 
       
   839 				options = options || {};
       
   840 
       
   841 				// Remove date_gmt if null.
       
   842 				if ( _.isNull( model.get( 'date_gmt' ) ) ) {
       
   843 					model.unset( 'date_gmt' );
       
   844 				}
       
   845 
       
   846 				// Remove slug if empty.
       
   847 				if ( _.isEmpty( model.get( 'slug' ) ) ) {
       
   848 					model.unset( 'slug' );
       
   849 				}
       
   850 
       
   851 				if ( _.isFunction( model.nonce ) && ! _.isEmpty( model.nonce() ) ) {
       
   852 					beforeSend = options.beforeSend;
       
   853 
       
   854 					// @todo enable option for jsonp endpoints
       
   855 					// options.dataType = 'jsonp';
       
   856 
       
   857 					// Include the nonce with requests.
       
   858 					options.beforeSend = function( xhr ) {
       
   859 						xhr.setRequestHeader( 'X-WP-Nonce', model.nonce() );
       
   860 
       
   861 						if ( beforeSend ) {
       
   862 							return beforeSend.apply( this, arguments );
       
   863 						}
       
   864 					};
       
   865 
       
   866 					// Update the nonce when a new nonce is returned with the response.
       
   867 					options.complete = function( xhr ) {
       
   868 						var returnedNonce = xhr.getResponseHeader( 'X-WP-Nonce' );
       
   869 
       
   870 						if ( returnedNonce && _.isFunction( model.nonce ) && model.nonce() !== returnedNonce ) {
       
   871 							model.endpointModel.set( 'nonce', returnedNonce );
       
   872 						}
       
   873 					};
       
   874 				}
       
   875 
       
   876 				// Add '?force=true' to use delete method when required.
       
   877 				if ( this.requireForceForDelete && 'delete' === method ) {
       
   878 					model.url = model.url() + '?force=true';
       
   879 				}
       
   880 				return Backbone.sync( method, model, options );
       
   881 			},
       
   882 
       
   883 			/**
       
   884 			 * Save is only allowed when the PUT OR POST methods are available for the endpoint.
       
   885 			 */
       
   886 			save: function( attrs, options ) {
       
   887 
       
   888 				// Do we have the put method, then execute the save.
       
   889 				if ( _.includes( this.methods, 'PUT' ) || _.includes( this.methods, 'POST' ) ) {
       
   890 
       
   891 					// Proxy the call to the original save function.
       
   892 					return Backbone.Model.prototype.save.call( this, attrs, options );
       
   893 				} else {
       
   894 
       
   895 					// Otherwise bail, disallowing action.
       
   896 					return false;
       
   897 				}
       
   898 			},
       
   899 
       
   900 			/**
       
   901 			 * Delete is only allowed when the DELETE method is available for the endpoint.
       
   902 			 */
       
   903 			destroy: function( options ) {
       
   904 
       
   905 				// Do we have the DELETE method, then execute the destroy.
       
   906 				if ( _.includes( this.methods, 'DELETE' ) ) {
       
   907 
       
   908 					// Proxy the call to the original save function.
       
   909 					return Backbone.Model.prototype.destroy.call( this, options );
       
   910 				} else {
       
   911 
       
   912 					// Otherwise bail, disallowing action.
       
   913 					return false;
       
   914 				}
       
   915 			}
       
   916 
       
   917 		}
       
   918 	);
       
   919 
       
   920 	/**
       
   921 	 * API Schema model. Contains meta information about the API.
       
   922 	 */
       
   923 	wp.api.models.Schema = wp.api.WPApiBaseModel.extend(
       
   924 		/** @lends Schema.prototype  */
       
   925 		{
       
   926 			defaults: {
       
   927 				_links: {},
       
   928 				namespace: null,
       
   929 				routes: {}
       
   930 			},
       
   931 
       
   932 			initialize: function( attributes, options ) {
       
   933 				var model = this;
       
   934 				options = options || {};
       
   935 
       
   936 				wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options );
       
   937 
       
   938 				model.apiRoot = options.apiRoot || wpApiSettings.root;
       
   939 				model.versionString = options.versionString || wpApiSettings.versionString;
       
   940 			},
       
   941 
       
   942 			url: function() {
       
   943 				return this.apiRoot + this.versionString;
       
   944 			}
       
   945 		}
       
   946 	);
       
   947 })();
       
   948 
       
   949 ( function() {
       
   950 
       
   951 	'use strict';
       
   952 
       
   953 	var wpApiSettings = window.wpApiSettings || {};
       
   954 
       
   955 	/**
       
   956 	 * Contains basic collection functionality such as pagination.
       
   957 	 */
       
   958 	wp.api.WPApiBaseCollection = Backbone.Collection.extend(
       
   959 		/** @lends BaseCollection.prototype  */
       
   960 		{
       
   961 
       
   962 			/**
       
   963 			 * Setup default state.
       
   964 			 */
       
   965 			initialize: function( models, options ) {
       
   966 				this.state = {
       
   967 					data: {},
       
   968 					currentPage: null,
       
   969 					totalPages: null,
       
   970 					totalObjects: null
       
   971 				};
       
   972 				if ( _.isUndefined( options ) ) {
       
   973 					this.parent = '';
       
   974 				} else {
       
   975 					this.parent = options.parent;
       
   976 				}
       
   977 			},
       
   978 
       
   979 			/**
       
   980 			 * Extend Backbone.Collection.sync to add nince and pagination support.
       
   981 			 *
       
   982 			 * Set nonce header before every Backbone sync.
       
   983 			 *
       
   984 			 * @param {string} method.
       
   985 			 * @param {Backbone.Model} model.
       
   986 			 * @param {{success}, *} options.
       
   987 			 * @returns {*}.
       
   988 			 */
       
   989 			sync: function( method, model, options ) {
       
   990 				var beforeSend, success,
       
   991 					self = this;
       
   992 
       
   993 				options = options || {};
       
   994 
       
   995 				if ( _.isFunction( model.nonce ) && ! _.isEmpty( model.nonce() ) ) {
       
   996 					beforeSend = options.beforeSend;
       
   997 
       
   998 					// Include the nonce with requests.
       
   999 					options.beforeSend = function( xhr ) {
       
  1000 						xhr.setRequestHeader( 'X-WP-Nonce', model.nonce() );
       
  1001 
       
  1002 						if ( beforeSend ) {
       
  1003 							return beforeSend.apply( self, arguments );
       
  1004 						}
       
  1005 					};
       
  1006 
       
  1007 					// Update the nonce when a new nonce is returned with the response.
       
  1008 					options.complete = function( xhr ) {
       
  1009 						var returnedNonce = xhr.getResponseHeader( 'X-WP-Nonce' );
       
  1010 
       
  1011 						if ( returnedNonce && _.isFunction( model.nonce ) && model.nonce() !== returnedNonce ) {
       
  1012 							model.endpointModel.set( 'nonce', returnedNonce );
       
  1013 						}
       
  1014 					};
       
  1015 				}
       
  1016 
       
  1017 				// When reading, add pagination data.
       
  1018 				if ( 'read' === method ) {
       
  1019 					if ( options.data ) {
       
  1020 						self.state.data = _.clone( options.data );
       
  1021 
       
  1022 						delete self.state.data.page;
       
  1023 					} else {
       
  1024 						self.state.data = options.data = {};
       
  1025 					}
       
  1026 
       
  1027 					if ( 'undefined' === typeof options.data.page ) {
       
  1028 						self.state.currentPage  = null;
       
  1029 						self.state.totalPages   = null;
       
  1030 						self.state.totalObjects = null;
       
  1031 					} else {
       
  1032 						self.state.currentPage = options.data.page - 1;
       
  1033 					}
       
  1034 
       
  1035 					success = options.success;
       
  1036 					options.success = function( data, textStatus, request ) {
       
  1037 						if ( ! _.isUndefined( request ) ) {
       
  1038 							self.state.totalPages   = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 );
       
  1039 							self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 );
       
  1040 						}
       
  1041 
       
  1042 						if ( null === self.state.currentPage ) {
       
  1043 							self.state.currentPage = 1;
       
  1044 						} else {
       
  1045 							self.state.currentPage++;
       
  1046 						}
       
  1047 
       
  1048 						if ( success ) {
       
  1049 							return success.apply( this, arguments );
       
  1050 						}
       
  1051 					};
       
  1052 				}
       
  1053 
       
  1054 				// Continue by calling Bacckbone's sync.
       
  1055 				return Backbone.sync( method, model, options );
       
  1056 			},
       
  1057 
       
  1058 			/**
       
  1059 			 * Fetches the next page of objects if a new page exists.
       
  1060 			 *
       
  1061 			 * @param {data: {page}} options.
       
  1062 			 * @returns {*}.
       
  1063 			 */
       
  1064 			more: function( options ) {
       
  1065 				options = options || {};
       
  1066 				options.data = options.data || {};
       
  1067 
       
  1068 				_.extend( options.data, this.state.data );
       
  1069 
       
  1070 				if ( 'undefined' === typeof options.data.page ) {
       
  1071 					if ( ! this.hasMore() ) {
       
  1072 						return false;
       
  1073 					}
       
  1074 
       
  1075 					if ( null === this.state.currentPage || this.state.currentPage <= 1 ) {
       
  1076 						options.data.page = 2;
       
  1077 					} else {
       
  1078 						options.data.page = this.state.currentPage + 1;
       
  1079 					}
       
  1080 				}
       
  1081 
       
  1082 				return this.fetch( options );
       
  1083 			},
       
  1084 
       
  1085 			/**
       
  1086 			 * Returns true if there are more pages of objects available.
       
  1087 			 *
       
  1088 			 * @returns null|boolean.
       
  1089 			 */
       
  1090 			hasMore: function() {
       
  1091 				if ( null === this.state.totalPages ||
       
  1092 					 null === this.state.totalObjects ||
       
  1093 					 null === this.state.currentPage ) {
       
  1094 					return null;
       
  1095 				} else {
       
  1096 					return ( this.state.currentPage < this.state.totalPages );
       
  1097 				}
       
  1098 			}
       
  1099 		}
       
  1100 	);
       
  1101 
       
  1102 } )();
       
  1103 
       
  1104 ( function() {
       
  1105 
       
  1106 	'use strict';
       
  1107 
       
  1108 	var Endpoint, initializedDeferreds = {},
       
  1109 		wpApiSettings = window.wpApiSettings || {};
       
  1110 
       
  1111 	/** @namespace wp */
       
  1112 	window.wp = window.wp || {};
       
  1113 
       
  1114 	/** @namespace wp.api */
       
  1115 	wp.api    = wp.api || {};
       
  1116 
       
  1117 	// If wpApiSettings is unavailable, try the default.
       
  1118 	if ( _.isEmpty( wpApiSettings ) ) {
       
  1119 		wpApiSettings.root = window.location.origin + '/wp-json/';
       
  1120 	}
       
  1121 
       
  1122 	Endpoint = Backbone.Model.extend(/** @lends Endpoint.prototype */{
       
  1123 		defaults: {
       
  1124 			apiRoot: wpApiSettings.root,
       
  1125 			versionString: wp.api.versionString,
       
  1126 			nonce: null,
       
  1127 			schema: null,
       
  1128 			models: {},
       
  1129 			collections: {}
       
  1130 		},
       
  1131 
       
  1132 		/**
       
  1133 		 * Initialize the Endpoint model.
       
  1134 		 */
       
  1135 		initialize: function() {
       
  1136 			var model = this, deferred;
       
  1137 
       
  1138 			Backbone.Model.prototype.initialize.apply( model, arguments );
       
  1139 
       
  1140 			deferred = jQuery.Deferred();
       
  1141 			model.schemaConstructed = deferred.promise();
       
  1142 
       
  1143 			model.schemaModel = new wp.api.models.Schema( null, {
       
  1144 				apiRoot:       model.get( 'apiRoot' ),
       
  1145 				versionString: model.get( 'versionString' ),
       
  1146 				nonce:         model.get( 'nonce' )
       
  1147 			} );
       
  1148 
       
  1149 			// When the model loads, resolve the promise.
       
  1150 			model.schemaModel.once( 'change', function() {
       
  1151 				model.constructFromSchema();
       
  1152 				deferred.resolve( model );
       
  1153 			} );
       
  1154 
       
  1155 			if ( model.get( 'schema' ) ) {
       
  1156 
       
  1157 				// Use schema supplied as model attribute.
       
  1158 				model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) );
       
  1159 			} else if (
       
  1160 				! _.isUndefined( sessionStorage ) &&
       
  1161 				( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) &&
       
  1162 				sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) )
       
  1163 			) {
       
  1164 
       
  1165 				// Used a cached copy of the schema model if available.
       
  1166 				model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) );
       
  1167 			} else {
       
  1168 				model.schemaModel.fetch( {
       
  1169 					/**
       
  1170 					 * When the server returns the schema model data, store the data in a sessionCache so we don't
       
  1171 					 * have to retrieve it again for this session. Then, construct the models and collections based
       
  1172 					 * on the schema model data.
       
  1173 					 *
       
  1174 					 * @callback
       
  1175 					 */
       
  1176 					success: function( newSchemaModel ) {
       
  1177 
       
  1178 						// Store a copy of the schema model in the session cache if available.
       
  1179 						if ( ! _.isUndefined( sessionStorage ) && ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) ) {
       
  1180 							try {
       
  1181 								sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) );
       
  1182 							} catch ( error ) {
       
  1183 
       
  1184 								// Fail silently, fixes errors in safari private mode.
       
  1185 							}
       
  1186 						}
       
  1187 					},
       
  1188 
       
  1189 					// Log the error condition.
       
  1190 					error: function( err ) {
       
  1191 						window.console.log( err );
       
  1192 					}
       
  1193 				} );
       
  1194 			}
       
  1195 		},
       
  1196 
       
  1197 		constructFromSchema: function() {
       
  1198 			var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects,
       
  1199 
       
  1200 			/**
       
  1201 			 * Set up the model and collection name mapping options. As the schema is built, the
       
  1202 			 * model and collection names will be adjusted if they are found in the mapping object.
       
  1203 			 *
       
  1204 			 * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options.
       
  1205 			 *
       
  1206 			 */
       
  1207 			mapping = wpApiSettings.mapping || {
       
  1208 				models: {
       
  1209 					'Categories':      'Category',
       
  1210 					'Comments':        'Comment',
       
  1211 					'Pages':           'Page',
       
  1212 					'PagesMeta':       'PageMeta',
       
  1213 					'PagesRevisions':  'PageRevision',
       
  1214 					'Posts':           'Post',
       
  1215 					'PostsCategories': 'PostCategory',
       
  1216 					'PostsRevisions':  'PostRevision',
       
  1217 					'PostsTags':       'PostTag',
       
  1218 					'Schema':          'Schema',
       
  1219 					'Statuses':        'Status',
       
  1220 					'Tags':            'Tag',
       
  1221 					'Taxonomies':      'Taxonomy',
       
  1222 					'Types':           'Type',
       
  1223 					'Users':           'User'
       
  1224 				},
       
  1225 				collections: {
       
  1226 					'PagesMeta':       'PageMeta',
       
  1227 					'PagesRevisions':  'PageRevisions',
       
  1228 					'PostsCategories': 'PostCategories',
       
  1229 					'PostsMeta':       'PostMeta',
       
  1230 					'PostsRevisions':  'PostRevisions',
       
  1231 					'PostsTags':       'PostTags'
       
  1232 				}
       
  1233 			},
       
  1234 
       
  1235 			modelEndpoints = routeModel.get( 'modelEndpoints' ),
       
  1236 			modelRegex     = new RegExp( '(?:.*[+)]|\/(' + modelEndpoints.join( '|' ) + '))$' );
       
  1237 
       
  1238 			/**
       
  1239 			 * Iterate thru the routes, picking up models and collections to build. Builds two arrays,
       
  1240 			 * one for models and one for collections.
       
  1241 			 */
       
  1242 			modelRoutes      = [];
       
  1243 			collectionRoutes = [];
       
  1244 			schemaRoot       = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' );
       
  1245 			loadingObjects   = {};
       
  1246 
       
  1247 			/**
       
  1248 			 * Tracking objects for models and collections.
       
  1249 			 */
       
  1250 			loadingObjects.models      = {};
       
  1251 			loadingObjects.collections = {};
       
  1252 
       
  1253 			_.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) {
       
  1254 
       
  1255 				// Skip the schema root if included in the schema.
       
  1256 				if ( index !== routeModel.get( ' versionString' ) &&
       
  1257 						index !== schemaRoot &&
       
  1258 						index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) )
       
  1259 				) {
       
  1260 
       
  1261 					// Single items end with a regex, or a special case word.
       
  1262 					if ( modelRegex.test( index ) ) {
       
  1263 						modelRoutes.push( { index: index, route: route } );
       
  1264 					} else {
       
  1265 
       
  1266 						// Collections end in a name.
       
  1267 						collectionRoutes.push( { index: index, route: route } );
       
  1268 					}
       
  1269 				}
       
  1270 			} );
       
  1271 
       
  1272 			/**
       
  1273 			 * Construct the models.
       
  1274 			 *
       
  1275 			 * Base the class name on the route endpoint.
       
  1276 			 */
       
  1277 			_.each( modelRoutes, function( modelRoute ) {
       
  1278 
       
  1279 				// Extract the name and any parent from the route.
       
  1280 				var modelClassName,
       
  1281 					routeName  = wp.api.utils.extractRoutePart( modelRoute.index, 2, routeModel.get( 'versionString' ), true ),
       
  1282 					parentName = wp.api.utils.extractRoutePart( modelRoute.index, 1, routeModel.get( 'versionString' ), false ),
       
  1283 					routeEnd   = wp.api.utils.extractRoutePart( modelRoute.index, 1, routeModel.get( 'versionString' ), true );
       
  1284 
       
  1285 				// Clear the parent part of the rouite if its actually the version string.
       
  1286 				if ( parentName === routeModel.get( 'versionString' ) ) {
       
  1287 					parentName = '';
       
  1288 				}
       
  1289 
       
  1290 				// Handle the special case of the 'me' route.
       
  1291 				if ( 'me' === routeEnd ) {
       
  1292 					routeName = 'me';
       
  1293 				}
       
  1294 
       
  1295 				// If the model has a parent in its route, add that to its class name.
       
  1296 				if ( '' !== parentName && parentName !== routeName ) {
       
  1297 					modelClassName = wp.api.utils.capitalizeAndCamelCaseDashes( parentName ) + wp.api.utils.capitalizeAndCamelCaseDashes( routeName );
       
  1298 					modelClassName = mapping.models[ modelClassName ] || modelClassName;
       
  1299 					loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
       
  1300 
       
  1301 						// Return a constructed url based on the parent and id.
       
  1302 						url: function() {
       
  1303 							var url =
       
  1304 								routeModel.get( 'apiRoot' ) +
       
  1305 								routeModel.get( 'versionString' ) +
       
  1306 								parentName +  '/' +
       
  1307 									( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ?
       
  1308 										( _.isUndefined( this.get( 'parent_post' ) ) ? '' : this.get( 'parent_post' ) + '/' ) :
       
  1309 										this.get( 'parent' ) + '/' ) +
       
  1310 								routeName;
       
  1311 
       
  1312 							if ( ! _.isUndefined( this.get( 'id' ) ) ) {
       
  1313 								url +=  '/' + this.get( 'id' );
       
  1314 							}
       
  1315 							return url;
       
  1316 						},
       
  1317 
       
  1318 						// Track nonces on the Endpoint 'routeModel'.
       
  1319 						nonce: function() {
       
  1320 							return routeModel.get( 'nonce' );
       
  1321 						},
       
  1322 
       
  1323 						endpointModel: routeModel,
       
  1324 
       
  1325 						// Include a reference to the original route object.
       
  1326 						route: modelRoute,
       
  1327 
       
  1328 						// Include a reference to the original class name.
       
  1329 						name: modelClassName,
       
  1330 
       
  1331 						// Include the array of route methods for easy reference.
       
  1332 						methods: modelRoute.route.methods,
       
  1333 
       
  1334 						// Include the array of route endpoints for easy reference.
       
  1335 						endpoints: modelRoute.route.endpoints
       
  1336 					} );
       
  1337 				} else {
       
  1338 
       
  1339 					// This is a model without a parent in its route
       
  1340 					modelClassName = wp.api.utils.capitalizeAndCamelCaseDashes( routeName );
       
  1341 					modelClassName = mapping.models[ modelClassName ] || modelClassName;
       
  1342 					loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( {
       
  1343 
       
  1344 						// Function that returns a constructed url based on the id.
       
  1345 						url: function() {
       
  1346 							var url = routeModel.get( 'apiRoot' ) +
       
  1347 								routeModel.get( 'versionString' ) +
       
  1348 								( ( 'me' === routeName ) ? 'users/me' : routeName );
       
  1349 
       
  1350 							if ( ! _.isUndefined( this.get( 'id' ) ) ) {
       
  1351 								url +=  '/' + this.get( 'id' );
       
  1352 							}
       
  1353 							return url;
       
  1354 						},
       
  1355 
       
  1356 						// Track nonces at the Endpoint level.
       
  1357 						nonce: function() {
       
  1358 							return routeModel.get( 'nonce' );
       
  1359 						},
       
  1360 
       
  1361 						endpointModel: routeModel,
       
  1362 
       
  1363 						// Include a reference to the original route object.
       
  1364 						route: modelRoute,
       
  1365 
       
  1366 						// Include a reference to the original class name.
       
  1367 						name: modelClassName,
       
  1368 
       
  1369 						// Include the array of route methods for easy reference.
       
  1370 						methods: modelRoute.route.methods,
       
  1371 
       
  1372 						// Include the array of route endpoints for easy reference.
       
  1373 						endpoints: modelRoute.route.endpoints
       
  1374 					} );
       
  1375 				}
       
  1376 
       
  1377 				// Add defaults to the new model, pulled form the endpoint.
       
  1378 				wp.api.utils.decorateFromRoute(
       
  1379 					modelRoute.route.endpoints,
       
  1380 					loadingObjects.models[ modelClassName ],
       
  1381 					routeModel.get( 'versionString' )
       
  1382 				);
       
  1383 
       
  1384 			} );
       
  1385 
       
  1386 			/**
       
  1387 			 * Construct the collections.
       
  1388 			 *
       
  1389 			 * Base the class name on the route endpoint.
       
  1390 			 */
       
  1391 			_.each( collectionRoutes, function( collectionRoute ) {
       
  1392 
       
  1393 				// Extract the name and any parent from the route.
       
  1394 				var collectionClassName, modelClassName,
       
  1395 						routeName  = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ),
       
  1396 						parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 1, routeModel.get( 'versionString' ), false );
       
  1397 
       
  1398 				// If the collection has a parent in its route, add that to its class name.
       
  1399 				if ( '' !== parentName && parentName !== routeName && routeModel.get( 'versionString' ) !== parentName ) {
       
  1400 
       
  1401 					collectionClassName = wp.api.utils.capitalizeAndCamelCaseDashes( parentName ) + wp.api.utils.capitalizeAndCamelCaseDashes( routeName );
       
  1402 					modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
       
  1403 					collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
       
  1404 					loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
       
  1405 
       
  1406 						// Function that returns a constructed url passed on the parent.
       
  1407 						url: function() {
       
  1408 							return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) +
       
  1409 									parentName + '/' + this.parent + '/' +
       
  1410 									routeName;
       
  1411 						},
       
  1412 
       
  1413 						// Specify the model that this collection contains.
       
  1414 						model: function( attrs, options ) {
       
  1415 							return new loadingObjects.models[ modelClassName ]( attrs, options );
       
  1416 						},
       
  1417 
       
  1418 						// Track nonces at the Endpoint level.
       
  1419 						nonce: function() {
       
  1420 							return routeModel.get( 'nonce' );
       
  1421 						},
       
  1422 
       
  1423 						endpointModel: routeModel,
       
  1424 
       
  1425 						// Include a reference to the original class name.
       
  1426 						name: collectionClassName,
       
  1427 
       
  1428 						// Include a reference to the original route object.
       
  1429 						route: collectionRoute,
       
  1430 
       
  1431 						// Include the array of route methods for easy reference.
       
  1432 						methods: collectionRoute.route.methods
       
  1433 					} );
       
  1434 				} else {
       
  1435 
       
  1436 					// This is a collection without a parent in its route.
       
  1437 					collectionClassName = wp.api.utils.capitalizeAndCamelCaseDashes( routeName );
       
  1438 					modelClassName      = mapping.models[ collectionClassName ] || collectionClassName;
       
  1439 					collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName;
       
  1440 					loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( {
       
  1441 
       
  1442 						// For the url of a root level collection, use a string.
       
  1443 						url: function() {
       
  1444 							return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName;
       
  1445 						},
       
  1446 
       
  1447 						// Specify the model that this collection contains.
       
  1448 						model: function( attrs, options ) {
       
  1449 							return new loadingObjects.models[ modelClassName ]( attrs, options );
       
  1450 						},
       
  1451 
       
  1452 						// Track nonces at the Endpoint level.
       
  1453 						nonce: function() {
       
  1454 							return routeModel.get( 'nonce' );
       
  1455 						},
       
  1456 
       
  1457 						endpointModel: routeModel,
       
  1458 
       
  1459 						// Include a reference to the original class name.
       
  1460 						name: collectionClassName,
       
  1461 
       
  1462 						// Include a reference to the original route object.
       
  1463 						route: collectionRoute,
       
  1464 
       
  1465 						// Include the array of route methods for easy reference.
       
  1466 						methods: collectionRoute.route.methods
       
  1467 					} );
       
  1468 				}
       
  1469 
       
  1470 				// Add defaults to the new model, pulled form the endpoint.
       
  1471 				wp.api.utils.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] );
       
  1472 			} );
       
  1473 
       
  1474 			// Add mixins and helpers for each of the models.
       
  1475 			_.each( loadingObjects.models, function( model, index ) {
       
  1476 				loadingObjects.models[ index ] = wp.api.utils.addMixinsAndHelpers( model, index, loadingObjects );
       
  1477 			} );
       
  1478 
       
  1479 			// Set the routeModel models and collections.
       
  1480 			routeModel.set( 'models', loadingObjects.models );
       
  1481 			routeModel.set( 'collections', loadingObjects.collections );
       
  1482 
       
  1483 		}
       
  1484 
       
  1485 	} );
       
  1486 
       
  1487 	wp.api.endpoints = new Backbone.Collection();
       
  1488 
       
  1489 	/**
       
  1490 	 * Initialize the wp-api, optionally passing the API root.
       
  1491 	 *
       
  1492 	 * @param {object} [args]
       
  1493 	 * @param {string} [args.nonce] The nonce. Optional, defaults to wpApiSettings.nonce.
       
  1494 	 * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root.
       
  1495 	 * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root.
       
  1496 	 * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided.
       
  1497 	 */
       
  1498 	wp.api.init = function( args ) {
       
  1499 		var endpoint, attributes = {}, deferred, promise;
       
  1500 
       
  1501 		args                      = args || {};
       
  1502 		attributes.nonce          = _.isString( args.nonce ) ? args.nonce : ( wpApiSettings.nonce || '' );
       
  1503 		attributes.apiRoot        = args.apiRoot || wpApiSettings.root || '/wp-json';
       
  1504 		attributes.versionString  = args.versionString || wpApiSettings.versionString || 'wp/v2/';
       
  1505 		attributes.schema         = args.schema || null;
       
  1506 		attributes.modelEndpoints = args.modelEndpoints || [ 'me', 'settings' ];
       
  1507 		if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) {
       
  1508 			attributes.schema = wpApiSettings.schema;
       
  1509 		}
       
  1510 
       
  1511 		if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) {
       
  1512 
       
  1513 			// Look for an existing copy of this endpoint
       
  1514 			endpoint = wp.api.endpoints.findWhere( { 'apiRoot': attributes.apiRoot, 'versionString': attributes.versionString } );
       
  1515 			if ( ! endpoint ) {
       
  1516 				endpoint = new Endpoint( attributes );
       
  1517 			}
       
  1518 			deferred = jQuery.Deferred();
       
  1519 			promise = deferred.promise();
       
  1520 
       
  1521 			endpoint.schemaConstructed.done( function( resolvedEndpoint ) {
       
  1522 				wp.api.endpoints.add( resolvedEndpoint );
       
  1523 
       
  1524 				// Map the default endpoints, extending any already present items (including Schema model).
       
  1525 				wp.api.models      = _.extend( wp.api.models, resolvedEndpoint.get( 'models' ) );
       
  1526 				wp.api.collections = _.extend( wp.api.collections, resolvedEndpoint.get( 'collections' ) );
       
  1527 				deferred.resolve( resolvedEndpoint );
       
  1528 			} );
       
  1529 			initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise;
       
  1530 		}
       
  1531 		return initializedDeferreds[ attributes.apiRoot + attributes.versionString ];
       
  1532 	};
       
  1533 
       
  1534 	/**
       
  1535 	 * Construct the default endpoints and add to an endpoints collection.
       
  1536 	 */
       
  1537 
       
  1538 	// The wp.api.init function returns a promise that will resolve with the endpoint once it is ready.
       
  1539 	wp.api.loadPromise = wp.api.init();
       
  1540 
       
  1541 } )();