wp-api.js 46 KB

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