From ec082f533d4b42d56f9478545670784e48e819f1 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Sat, 9 Jan 2016 10:58:44 -0700 Subject: [PATCH 1/5] Add latest client js including minified version * Full js client development log at https://github.com/WP-API/client-js * When SCRIPT_DEBUG off, use minified script Change highlights: * Api builds models and collections from Schema, no hard coded models or collections; any endpoint in the schema will be mapped; supports multiple schemas (apis); helper methods for post model: all helpers user _embed data if available, otherwise using ajax request. * new: Async load of api, apps can use the deferred to time startup * new: Ability to localize schema, falls back to making an ajax request & cached in session storage; develop branch of client js has code that only localizes data once per session * new Helper methods for Posts model: getCategories and setCategories * fix: cleanup TimeStampedMixin * new: models/collections get defaults and options from schema * fix: all code up to WordPress JavaScript coding standards * new: helper method to get post author user model `getAuthorUser` * new: helper method to get a featured image for a post: `getFeaturedImage` * new: models protect methods based on endpoint methods (eg. destory is blocked if an endpoint doesn't have a 'DELETE' method) * new: models contain a reference to the original route object --- extras.php | 6 +- wp-api.js | 1454 +++++++++++++++++++++++++++--------------------- wp-api.min.js | 2 + wp-api.min.map | 1 + 4 files changed, 817 insertions(+), 646 deletions(-) mode change 100755 => 100644 wp-api.js create mode 100644 wp-api.min.js create mode 100644 wp-api.min.map diff --git a/extras.php b/extras.php index 737c35d242..d1fe83d638 100755 --- a/extras.php +++ b/extras.php @@ -19,7 +19,11 @@ * @see wp_register_scripts() */ function rest_register_scripts() { - wp_register_script( 'wp-api', plugins_url( 'wp-api.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); + + // Use minified scripts if SCRIPT_DEBUG is not on. + $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; + + wp_register_script( 'wp-api', plugins_url( 'wp-api' . $suffix . '.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); $settings = array( 'root' => esc_url_raw( get_rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ) ); wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); diff --git a/wp-api.js b/wp-api.js old mode 100755 new mode 100644 index e2548cf3e7..4d8382760e --- a/wp-api.js +++ b/wp-api.js @@ -8,8 +8,9 @@ this.views = {}; } - window.wp = window.wp || {}; - wp.api = wp.api || new WP_API(); + window.wp = window.wp || {}; + wp.api = wp.api || new WP_API(); + wp.api.versionString = wp.api.versionString || 'wp/v2/'; })( window ); @@ -17,18 +18,20 @@ 'use strict'; + var pad, r; + window.wp = window.wp || {}; wp.api = wp.api || {}; wp.api.utils = wp.api.utils || {}; /** - * ECMAScript 5 shim, from MDN. + * ECMAScript 5 shim, adapted from MDN. * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString */ if ( ! Date.prototype.toISOString ) { - var pad = function( number ) { - var r = String( number ); - if ( r.length === 1 ) { + pad = function( number ) { + r = String( number ); + if ( 1 === r.length ) { r = '0' + r; } @@ -62,6 +65,7 @@ // implementations could be faster. // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 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 ) ) ) { + // Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC. for ( i = 0; ( k = numericKeys[i] ); ++i ) { struct[k] = +struct[k] || 0; @@ -71,10 +75,10 @@ struct[2] = ( +struct[2] || 1 ) - 1; struct[3] = +struct[3] || 1; - if ( struct[8] !== 'Z' && struct[9] !== undefined ) { + if ( 'Z' !== struct[8] && undefined !== struct[9] ) { minutesOffset = struct[10] * 60 + struct[11]; - if ( struct[9] === '+' ) { + if ( '+' === struct[9] ) { minutesOffset = 0 - minutesOffset; } } @@ -87,125 +91,80 @@ return timestamp; }; -})( window ); - -/* global WP_API_Settings:false */ -// Suppress warning about parse function's unused "options" argument: -/* jshint unused:false */ -(function( wp, WP_API_Settings, Backbone, window, undefined ) { - - 'use strict'; + /** + * Helper function for getting the root URL. + * @return {[type]} [description] + */ + wp.api.utils.getRootUrl = function() { + return window.location.origin ? + window.location.origin + '/' : + window.location.protocol + '/' + window.location.host + '/'; + }; /** - * Array of parseable dates. - * - * @type {string[]}. + * Helper for capitalizing strings. */ - var parseable_dates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ]; + wp.api.utils.capitalize = function( str ) { + if ( _.isUndefined( str ) ) { + return str; + } + return str.charAt( 0 ).toUpperCase() + str.slice( 1 ); + }; /** - * Mixin for all content that is time stamped. + * Extract a route part based on negitive index. * - * @type {{toJSON: toJSON, parse: parse}}. + * @param {string} route The endpoint route. + * @param {int} part The number of parts from the end of the route to retrieve. Default 1. + * Example route `/a/b/c`: part 1 is `c`, part 2 is `b`, part 3 is `a`. */ - var TimeStampedMixin = { - /** - * Serialize the entity pre-sync. - * - * @returns {*}. - */ - toJSON: function() { - var attributes = _.clone( this.attributes ); - - // Serialize Date objects back into 8601 strings. - _.each( parseable_dates, function( key ) { - if ( key in attributes ) { - attributes[key] = attributes[key].toISOString(); - } - }); - - return attributes; - }, - - /** - * Unserialize the fetched response. - * - * @param {*} response. - * @returns {*}. - */ - parse: function( response ) { - - // Parse dates into native Date objects. - _.each( parseable_dates, function ( key ) { - if ( ! ( key in response ) ) { - return; - } - - var timestamp = wp.api.utils.parseISO8601( response[key] ); - response[key] = new Date( timestamp ); - }); + wp.api.utils.extractRoutePart = function( route, part ) { + var routeParts; - // Parse the author into a User object. - if ( 'undefined' !== typeof response.author ) { - response.author = new wp.api.models.User( response.author ); - } + part = part || 1; - return response; + // Remove versions string from route to avoid returning it. + route = route.replace( wp.api.versionString, '' ); + routeParts = route.split( '/' ).reverse(); + if ( _.isUndefined( routeParts[ --part ] ) ) { + return ''; } + return routeParts[ part ]; }; /** - * Mixin for all hierarchical content types such as posts. + * Extract a parent name from a passed route. * - * @type {{parent: parent}}. + * @param {string} route The route to extract a name from. */ - var HierarchicalMixin = { - /** - * Get parent object. - * - * @returns {Backbone.Model} - */ - parent: function() { + wp.api.utils.extractParentName = function( route ) { + var name, + lastSlash = route.lastIndexOf( '_id>[\\d]+)/' ); - var object, parent = this.get( 'parent' ); - - // Return null if we don't have a parent. - if ( parent === 0 ) { - return null; - } - - var parentModel = this; - - if ( 'undefined' !== typeof this.parentModel ) { - /** - * Probably a better way to do this. Perhaps grab a cached version of the - * instantiated model? - */ - parentModel = new this.parentModel(); - } + if ( lastSlash < 0 ) { + return ''; + } + name = route.substr( 0, lastSlash - 1 ); + name = name.split( '/' ); + name.pop(); + name = name.pop(); + return name; + }; - // Can we get this from its collection? - if ( parentModel.collection ) { - return parentModel.collection.get( parent ); - } else { +})( window ); - // Otherwise, get the object directly. - object = new parentModel.constructor( { - id: parent - }); +/* global wpApiSettings:false */ - // Note that this acts asynchronously. - object.fetch(); +// Suppress warning about parse function's unused "options" argument: +/* jshint unused:false */ +(function( wp, wpApiSettings, Backbone, window, undefined ) { - return object; - } - } - }; + 'use strict'; /** - * Private Backbone base model for all models. + * Backbone base model for all models. */ - var WPApiBaseModel = Backbone.Model.extend( + wp.api.WPApiBaseModel = Backbone.Model.extend( /** @lends WPApiBaseModel.prototype */ { /** @@ -217,13 +176,18 @@ * @returns {*}. */ sync: function( method, model, options ) { + var beforeSend; + options = options || {}; - if ( 'undefined' !== typeof WP_API_Settings.nonce ) { - var beforeSend = options.beforeSend; + if ( ! _.isUndefined( wpApiSettings.nonce ) && ! _.isNull( wpApiSettings.nonce ) ) { + beforeSend = options.beforeSend; + + // @todo enable option for jsonp endpoints + // options.dataType = 'jsonp'; options.beforeSend = function( xhr ) { - xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce ); + xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); if ( beforeSend ) { return beforeSend.apply( this, arguments ); @@ -231,441 +195,106 @@ }; } + // Add '?force=true' to delete method when required. + if ( this.requireForceForDelete && 'delete' === method ) { + model.url = model.url() + '?force=true'; + } return Backbone.sync( method, model, options ); - } - } - ); - - /** - * Backbone model for a single user. - * - * - * @param {Object} attributes - * @param {int} attributes.id The user id. Optional. Defaults to 'me', fetching the current user. - */ - wp.api.models.User = WPApiBaseModel.extend( - /** @lends User.prototype */ - { - idAttribute: 'id', - - urlRoot: WP_API_Settings.root + 'wp/v2/users', - - defaults: { - id: 'me', - avatar_url: {}, - capabilities: {}, - description: '', - email: '', - extra_capabilities: {}, - first_name: '', - last_name: '', - link: '', - name: '', - nickname: '', - registered_date: new Date(), - roles: [], - slug: '', - url: '', - username: '', - _links: {} - } - } - ); - - /** - * Model for a single taxonomy. - * - * @param {Object} attributes - * @param {string} attributes.slug The taxonomy slug. - */ - wp.api.models.Taxonomy = WPApiBaseModel.extend( - /** @lends Taxonomy.prototype */ - { - idAttribute: 'slug', - - urlRoot: WP_API_Settings.root + 'wp/v2/taxonomies', - - defaults: { - name: '', - slug: null, - description: '', - labels: {}, - types: [], - show_cloud: false, - hierarchical: false - } - } - ); - - /** - * Backbone model for a single term. - * - * @param {Object} attributes - * @param {int} id attributesm id. - */ - wp.api.models.Term = WPApiBaseModel.extend( - /** @lends Term.prototype */ - { - idAttribute: 'id', - - urlRoot: WP_API_Settings.root + 'wp/v2/terms/tag', - - defaults: { - id: null, - name: '', - slug: '', - description: '', - parent: null, - count: 0, - link: '', - taxonomy: '', - _links: {} - } - - } - ); - - /** - * Backbone model for a single post. - * - * @param {Object} attributes - * @param {int} attributes.id The post id. - */ - wp.api.models.Post = WPApiBaseModel.extend( _.extend( - /** @lends Post.prototype */ - { - idAttribute: 'id', - - urlRoot: WP_API_Settings.root + 'wp/v2/posts', - - defaults: { - id: null, - date: new Date(), - date_gmt: new Date(), - guid: {}, - link: '', - modified: new Date(), - modified_gmt: new Date(), - password: '', - status: 'draft', - type: 'post', - title: {}, - content: {}, - author: null, - excerpt: {}, - featured_image: null, - comment_status: 'open', - ping_status: 'open', - sticky: false, - format: 'standard', - _links: {} - } - }, TimeStampedMixin, HierarchicalMixin ) - ); - - /** - * Backbone model for a single page. - * - * @param {Object} attributes - * @param {int} attributes.id The page id. - */ - wp.api.models.Page = WPApiBaseModel.extend( _.extend( - /** @lends Page.prototype */ - { - idAttribute: 'id', - - urlRoot: WP_API_Settings.root + 'wp/v2/pages', - - defaults: { - id: null, - date: new Date(), - date_gmt: new Date(), - guid: {}, - link: '', - modified: new Date(), - modified_gmt: new Date(), - password: '', - slug: '', - status: 'draft', - type: 'page', - title: {}, - content: {}, - author: null, - excerpt: {}, - featured_image: null, - comment_status: 'closed', - ping_status: 'closed', - menu_order: null, - template: '', - _links: {} - } - }, TimeStampedMixin, HierarchicalMixin ) - ); - - /** - * Backbone model for a single post revision. - * - * @param {Object} attributes - * @param {int} attributes.parent The id of the post that this revision belongs to. - * @param {int} attributes.id The revision id. - */ - wp.api.models.PostRevision = WPApiBaseModel.extend( _.extend( - /** @lends PostRevision.prototype */ - { - idAttribute: 'id', - - defaults: { - id: null, - author: null, - date: new Date(), - date_gmt: new Date(), - guid: {}, - modified: new Date(), - modified_gmt: new Date(), - parent: 0, - slug: '', - title: {}, - content: {}, - excerpt: {}, - _links: {} }, /** - * Return URL for the model. - * - * @returns {string}. + * Save is only allowed when the PUT OR POST methods are available for the endpoint. */ - url: function() { - var id = this.get( 'id' ) || '', - parent = this.get( 'parent' ) || ''; - - return WP_API_Settings.root + 'wp/v2/posts/' + parent + '/revisions/' + id; - } - - }, TimeStampedMixin, HierarchicalMixin ) - ); - - /** - * Backbone model for a single media item. - * - * @param {Object} attributes - * @param {int} attributes.id The media item id. - */ - wp.api.models.Media = WPApiBaseModel.extend( _.extend( - /** @lends Media.prototype */ - { - idAttribute: 'id', - - urlRoot: WP_API_Settings.root + 'wp/v2/media', - - defaults: { - id: null, - date: new Date(), - date_gmt: new Date(), - guid: {}, - link: '', - modified: new Date(), - modified_gmt: new Date(), - password: '', - slug: '', - status: 'draft', - type: 'attachment', - title: {}, - author: null, - comment_status: 'open', - ping_status: 'open', - alt_text: '', - caption: '', - description: '', - media_type: '', - media_details: {}, - post: null, - source_url: '', - _links: {} - } - - }, TimeStampedMixin ) - ); - - /** - * Backbone model for a single comment. - * - * @param {Object} attributes - * @param {int} attributes.id The comment id. - */ - wp.api.models.Comment = WPApiBaseModel.extend( _.extend( - /** @lends Comment.prototype */ - { - idAttribute: 'id', - - urlRoot: WP_API_Settings.root + 'wp/v2/comments', - - defaults: { - id: null, - author: null, - author_email: '', - author_ip: '', - author_name: '', - author_url: '', - author_user_agent: '', - content: {}, - date: new Date(), - date_gmt: new Date(), - karma: 0, - link: '', - parent: 0, - status: 'hold', - type: '', - _links: {} - } + save: function( attrs, options ) { - }, TimeStampedMixin, HierarchicalMixin ) - ); + // Do we have the put method, then execute the save. + if ( _.contains( this.methods, 'PUT' ) || _.contains( this.methods, 'POST' ) ) { - /** - * Backbone model for a single post type. - * - * @param {Object} attributes - * @param {string} attributes.slug The post type slug. - */ - wp.api.models.PostType = WPApiBaseModel.extend( - /** @lends PostType.prototype */ - { - idAttribute: 'slug', - - urlRoot: WP_API_Settings.root + 'wp/v2/types', + // Proxy the call to the original save function. + return Backbone.Model.prototype.save.call( this, attrs, options ); + } else { - defaults: { - slug: null, - name: '', - description: '', - labels: {}, - hierarchical: false + // Otherwise bail, disallowing action. + return false; + } }, /** - * Prevent model from being saved. - * - * @returns {boolean}. + * Delete is only allowed when the DELETE method is available for the endpoint. */ - save: function() { - return false; - }, + destroy: function( options ) { - /** - * Prevent model from being deleted. - * - * @returns {boolean}. - */ - destroy: function() { - return false; - } - } - ); + // Do we have the DELETE method, then execute the destroy. + if ( _.contains( this.methods, 'DELETE' ) ) { - /** - * Backbone model for a a single post status. - * - * @param {Object} attributes - * @param {string} attributes.slug The post status slug. - */ - wp.api.models.PostStatus = WPApiBaseModel.extend( - /** @lends PostStatus.prototype */ - { - idAttribute: 'slug', - - urlRoot: WP_API_Settings.root + 'wp/v2/statuses', - - defaults: { - slug: null, - name: '', - 'public': true, - 'protected': false, - 'private': false, - queryable: true, - show_in_list: true, - _links: {} - }, - - /** - * Prevent model from being saved. - * - * @returns {boolean}. - */ - save: function() { - return false; - }, + // Proxy the call to the original save function. + return Backbone.Model.prototype.destroy.call( this, options ); + } else { - /** - * Prevent model from being deleted. - * - * @returns {boolean}. - */ - destroy: function() { - return false; + // Otherwise bail, disallowing action. + return false; + } } + } ); /** * API Schema model. Contains meta information about the API. */ - wp.api.models.Schema = WPApiBaseModel.extend( - /** @lends Shema.prototype */ + wp.api.models.Schema = wp.api.WPApiBaseModel.extend( + /** @lends Schema.prototype */ { - url: WP_API_Settings.root + 'wp/v2', - defaults: { - namespace: '', - _links: '', + _links: {}, + namespace: null, routes: {} }, - /** - * Prevent model from being saved. - * - * @returns {boolean}. - */ - save: function() { - return false; + initialize: function( attributes, options ) { + var model = this; + options = options || {}; + + wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options ); + + model.apiRoot = options.apiRoot || wpApiSettings.root; + model.versionString = options.versionString || wpApiSettings.versionString; }, - /** - * Prevent model from being deleted. - * - * @returns {boolean}. - */ - destroy: function() { - return false; + url: function() { + return this.apiRoot + this.versionString; } } ); +})( wp, wpApiSettings, Backbone, window ); - -})( wp, WP_API_Settings, Backbone, window ); - -/* global WP_API_Settings:false */ -(function( wp, WP_API_Settings, Backbone, _, window, undefined ) { +/* global wpApiSettings:false */ +(function( wp, wpApiSettings, Backbone, _, window, undefined ) { 'use strict'; /** * Contains basic collection functionality such as pagination. */ - var BaseCollection = Backbone.Collection.extend( + wp.api.WPApiBaseCollection = Backbone.Collection.extend( /** @lends BaseCollection.prototype */ { /** * Setup default state. */ - initialize: function() { + initialize: function( models, options ) { this.state = { data: {}, currentPage: null, totalPages: null, totalObjects: null }; + if ( _.isUndefined( options ) ) { + this.parent = ''; + } else { + this.parent = options.parent; + } }, /** @@ -679,13 +308,15 @@ * @returns {*}. */ sync: function( method, model, options ) { - options = options || {}; - var beforeSend = options.beforeSend, + var beforeSend, success, self = this; - if ( 'undefined' !== typeof WP_API_Settings.nonce ) { + options = options || {}; + beforeSend = options.beforeSend; + + if ( 'undefined' !== typeof wpApiSettings.nonce ) { options.beforeSend = function( xhr ) { - xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce ); + xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); if ( beforeSend ) { return beforeSend.apply( self, arguments ); @@ -710,12 +341,12 @@ self.state.currentPage = options.data.page - 1; } - var success = options.success; + success = options.success; options.success = function( data, textStatus, request ) { self.state.totalPages = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 ); self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 ); - if ( self.state.currentPage === null ) { + if ( null === self.state.currentPage ) { self.state.currentPage = 1; } else { self.state.currentPage++; @@ -747,7 +378,7 @@ return false; } - if ( this.state.currentPage === null || this.state.currentPage <= 1 ) { + if ( null === this.state.currentPage || this.state.currentPage <= 1 ) { options.data.page = 2; } else { options.data.page = this.state.currentPage + 1; @@ -763,9 +394,9 @@ * @returns null|boolean. */ hasMore: function() { - if ( this.state.totalPages === null || - this.state.totalObjects === null || - this.state.currentPage === null ) { + if ( null === this.state.totalPages || + null === this.state.totalObjects || + null === this.state.currentPage ) { return null; } else { return ( this.state.currentPage < this.state.totalPages ); @@ -774,209 +405,742 @@ } ); - /** - * Backbone collection for posts. - */ - wp.api.collections.Posts = BaseCollection.extend( - /** @lends Posts.prototype */ - { - url: WP_API_Settings.root + 'wp/v2/posts', +})( wp, wpApiSettings, Backbone, _, window ); - model: wp.api.models.Post - } - ); +/* global wpApiSettings */ +(function( window, undefined ) { - /** - * Backbone collection for pages. - */ - wp.api.collections.Pages = BaseCollection.extend( - /** @lends Pages.prototype */ - { - url: WP_API_Settings.root + 'wp/v2/pages', + 'use strict'; - model: wp.api.models.Page - } - ); + var Endpoint, initializedDeferreds = {}; - /** - * Backbone users collection. - */ - wp.api.collections.Users = BaseCollection.extend( - /** @lends Users.prototype */ - { - url: WP_API_Settings.root + 'wp/v2/users', + window.wp = window.wp || {}; + wp.api = wp.api || {}; - model: wp.api.models.User - } - ); + Endpoint = Backbone.Model.extend({ + defaults: { + apiRoot: wpApiSettings.root, + versionString: wp.api.versionString, + schema: null, + models: {}, + collections: {} + }, - /** - * Backbone post statuses collection. - */ - wp.api.collections.PostStatuses = BaseCollection.extend( - /** @lends PostStatuses.prototype */ - { - url: WP_API_Settings.root + 'wp/v2/statuses', + initialize: function() { + var model = this, deferred; + + Backbone.Model.prototype.initialize.apply( model, arguments ); + + deferred = jQuery.Deferred(); + model.schemaConstructed = deferred.promise(); + + model.schemaModel = new wp.api.models.Schema( null, { + apiRoot: model.get( 'apiRoot' ), + versionString: model.get( 'versionString' ) + }); + + model.schemaModel.once( 'change', function() { + model.constructFromSchema(); + deferred.resolve( model ); + } ); + + if ( model.get( 'schema' ) ) { + + // Use schema supplied as model attribute. + model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) ); + } else if ( ! _.isUndefined( sessionStorage ) && sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) { + + // Used a cached copy of the schema model if available. + model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) ); + } else { + model.schemaModel.fetch({ + /** + * When the server return the schema model data, store the data in a sessionCache so we don't + * have to retrieve it again for this session. Then, construct the models and collections based + * on the schema model data. + */ + success: function( newSchemaModel ) { + + // Store a copy of the schema model in the session cache if available. + if ( ! _.isUndefined( sessionStorage ) ) { + sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) ); + } + }, + + // @todo Handle the error condition. + error: function() { + } + }); + } + }, - model: wp.api.models.PostStatus, + constructFromSchema: function() { + var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects; - parse: function( response ) { - var responseArray = []; + /** + * Iterate thru the routes, picking up models and collections to build. Builds two arrays, + * one for models and one for collections. + */ + modelRoutes = []; + collectionRoutes = []; + schemaRoot = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' ); + loadingObjects = {}; + + /** + * Tracking objects for models and collections. + */ + loadingObjects.models = routeModel.get( 'models' ); + loadingObjects.collections = routeModel.get( 'collections' ); + + _.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) { + + // Skip the schema root if included in the schema. + if ( index !== routeModel.get( ' versionString' ) && + index !== schemaRoot && + index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) ) + ) { + /** + * Single item models end with a regex/variable. + * + * @todo make model/collection logic more robust. + */ + if ( index.endsWith( '+)' ) ) { + modelRoutes.push( { index: index, route: route } ); + } else { - for ( var property in response ) { - if ( response.hasOwnProperty( property ) ) { - responseArray.push( response[property] ); + // Collections end in a name. + if ( ! index.endsWith( 'me' ) ) { + collectionRoutes.push( { index: index, route: route } ); + } } } + } ); - return this.constructor.__super__.parse.call( this, responseArray ); - } - } - ); + /** + * Construct the models. + * + * Base the class name on the route endpoint. + */ + _.each( modelRoutes, function( modelRoute ) { + + // Extract the name and any parent from the route. + var modelClassName, + routeName = wp.api.utils.extractRoutePart( modelRoute.index, 2 ), + parentName = wp.api.utils.extractRoutePart( modelRoute.index, 4 ); + + // If the model has a parent in its route, add that to its class name. + if ( '' !== parentName && parentName !== routeName ) { + modelClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); + loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { + + // Function that returns a constructed url based on the parent and id. + url: function() { + var url = routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + + parentName + '/' + + ( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ? + this.get( 'parent_post' ) : + this.get( 'parent' ) ) + '/' + + routeName; + if ( ! _.isUndefined( this.get( 'id' ) ) ) { + url += '/' + this.get( 'id' ); + } + return url; + }, + + // Include a reference to the original route object. + route: modelRoute, + + // Include a reference to the original class name. + name: modelClassName, + + // Include the array of route methods for easy reference. + methods: modelRoute.route.methods, + + initialize: function() { + /** + * Posts and pages support trashing, other types don't support a trash + * and require that you pass ?force=true to actually delete them. + * + * @todo we should be getting trashability from the Schema, not hard coding types here. + */ + if ( + 'Posts' !== this.name && + 'Pages' !== this.name && + _.contains( this.methods, 'DELETE' ) + ) { + this.requireForceForDelete = true; + } + } + } ); + } else { - /** - * Backbone media library collection. - */ - wp.api.collections.MediaLibrary = BaseCollection.extend( - /** @lends MediaLibrary.prototype */ - { - url: WP_API_Settings.root + 'wp/v2/media', + // This is a model without a parent in its route + modelClassName = wp.api.utils.capitalize( routeName ); + loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { - model: wp.api.models.Media - } - ); + // Function that returns a constructed url based on the id. + url: function() { + var url = routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName; + if ( ! _.isUndefined( this.get( 'id' ) ) ) { + url += '/' + this.get( 'id' ); + } + return url; + }, - /** - * Backbone taxonomy collection. - */ - wp.api.collections.Taxonomies = BaseCollection.extend( - /** @lends Taxonomies.prototype */ - { - model: wp.api.models.Taxonomy, + // Include a reference to the original route object. + route: modelRoute, - url: WP_API_Settings.root + 'wp/v2/taxonomies' - } - ); + // Include a reference to the original class name. + name: modelClassName, - /** - * Backbone comment collection. - */ - wp.api.collections.Comments = BaseCollection.extend( - /** @lends Comments.prototype */ - { - model: wp.api.models.Comment, + // Include the array of route methods for easy reference. + methods: modelRoute.route.methods + } ); + } + + // Add defaults to the new model, pulled form the endpoint + wp.api.decorateFromRoute( modelRoute.route.endpoints, loadingObjects.models[ modelClassName ] ); + + } ); /** - * Return URL for collection. + * Construct the collections. * - * @returns {string}. + * Base the class name on the route endpoint. */ - url: WP_API_Settings.root + 'wp/v2/comments' - } - ); + _.each( collectionRoutes, function( collectionRoute ) { - /** - * Backbone post type collection. - */ - wp.api.collections.PostTypes = BaseCollection.extend( - /** @lends PostTypes.prototype */ - { - model: wp.api.models.PostType, + // Extract the name and any parent from the route. + var collectionClassName, + routeName = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ), + parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 3 ); - url: WP_API_Settings.root + 'wp/v2/types', + // If the collection has a parent in its route, add that to its class name/ + if ( '' !== parentName && parentName !== routeName ) { - parse: function( response ) { - var responseArray = []; + collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); + loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { - for ( var property in response ) { - if ( response.hasOwnProperty( property ) ) { - responseArray.push( response[property] ); - } + // Function that returns a constructed url passed on the parent. + url: function() { + return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + + parentName + '/' + this.parent + '/' + + routeName; + }, + + // Specify the model that this collection contains. + model: loadingObjects.models[ collectionClassName ], + + // Include a reference to the original class name. + name: collectionClassName, + + // Include a reference to the original route object. + route: collectionRoute, + + // Include the array of route methods for easy reference. + methods: collectionRoute.route.methods + } ); + } else { + + // This is a collection without a parent in its route. + collectionClassName = wp.api.utils.capitalize( routeName ); + loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { + + // For the url of a root level collection, use a string. + url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName, + + // Specify the model that this collection contains. + model: loadingObjects.models[ collectionClassName ], + + // Include a reference to the original class name. + name: collectionClassName, + + // Include a reference to the original route object. + route: collectionRoute, + + // Include the array of route methods for easy reference. + methods: collectionRoute.route.methods + } ); } - return this.constructor.__super__.parse.call( this, responseArray ); + // Add defaults to the new model, pulled form the endpoint + wp.api.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] ); + } ); + + // Add mixins and helpers for each of the models. + _.each( loadingObjects.models, function( model, index ) { + loadingObjects.models[ index ] = wp.api.addMixinsAndHelpers( model, index, loadingObjects ); + } ); + + } + + }); + + wp.api.endpoints = new Backbone.Collection({ + model: Endpoint + }); + + /** + * Initialize the wp-api, optionally passing the API root. + * + * @param {object} [args] + * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root. + * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root. + * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided. + */ + wp.api.init = function( args ) { + var endpoint, attributes = {}, deferred, promise; + + args = args || {}; + attributes.apiRoot = args.apiRoot || wpApiSettings.root; + attributes.versionString = args.versionString || wpApiSettings.versionString; + attributes.schema = args.schema || null; + if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) { + attributes.schema = wpApiSettings.schema; + } + + if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) { + endpoint = wp.api.endpoints.findWhere( { apiRoot: attributes.apiRoot, versionString: attributes.versionString } ); + if ( ! endpoint ) { + endpoint = new Endpoint( attributes ); + wp.api.endpoints.add( endpoint ); } + deferred = jQuery.Deferred(); + promise = deferred.promise(); + + endpoint.schemaConstructed.done( function( endpoint ) { + + // Map the default endpoints, extending any already present items (including Schema model). + wp.api.models = _.extend( endpoint.get( 'models' ), wp.api.models ); + wp.api.collections = _.extend( endpoint.get( 'collections' ), wp.api.collections ); + deferred.resolveWith( wp.api, [ endpoint ] ); + } ); + initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise; } - ); + return initializedDeferreds[ attributes.apiRoot + attributes.versionString ]; + }; /** - * Backbone terms collection. + * Add mixins and helpers to models depending on their defaults. * - * Usage: new wp.api.collections.Terms( {}, { taxonomy: 'taxonomy-slug' } ) + * @param {Backbone Model} model The model to attach helpers and mixins to. + * @param {string} modelClassName The classname of the constructed model. + * @param {Object} loadingObjects An object containing the models and collections we are building. */ - wp.api.collections.Terms = BaseCollection.extend( - /** @lends Terms.prototype */ - { - model: wp.api.models.Term, + wp.api.addMixinsAndHelpers = function( model, modelClassName, loadingObjects ) { - taxonomy: 'category', + var hasDate = false, /** - * @class Represent an array of terms. - * @augments Backbone.Collection. - * @constructs + * Array of parseable dates. + * + * @type {string[]}. */ - initialize: function( models, options ) { - if ( 'undefined' !== typeof options && options.taxonomy ) { - this.taxonomy = options.taxonomy; - } + parseableDates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ], - BaseCollection.prototype.initialize.apply( this, arguments ); + /** + * Mixin for all content that is time stamped. + * + * This mixin converts between mysql timestamps and JavaScript Dates when syncing a model + * to or from the server. For example, a date stored as `2015-12-27T21:22:24` on the server + * gets expanded to `Sun Dec 27 2015 14:22:24 GMT-0700 (MST)` when the model is fetched. + * + * @type {{toJSON: toJSON, parse: parse}}. + */ + TimeStampedMixin = { + /** + * Serialize the entity pre-sync. + * + * @returns {*}. + */ + toJSON: function() { + var attributes = _.clone( this.attributes ); + + // Serialize Date objects back into 8601 strings. + _.each( parseableDates, function( key ) { + if ( key in attributes ) { + + // Don't convert null values + if ( ! _.isNull( attributes[ key ] ) ) { + attributes[ key ] = attributes[ key ].toISOString(); + } + } + } ); + + return attributes; + }, + + /** + * Unserialize the fetched response. + * + * @param {*} response. + * @returns {*}. + */ + parse: function( response ) { + var timestamp; + + // Parse dates into native Date objects. + _.each( parseableDates, function( key ) { + if ( ! ( key in response ) ) { + return; + } + + // Don't convert null values + if ( ! _.isNull( response[ key ] ) ) { + timestamp = wp.api.utils.parseISO8601( response[ key ] ); + response[ key ] = new Date( timestamp ); + } + }); + + return response; + } }, /** - * Return URL for collection. - * - * @returns {string}. + * Add a helper funtion to handle post Categories. */ - url: function() { - return WP_API_Settings.root + 'wp/v2/terms/' + this.taxonomy; - } - } - ); + CategoriesMixin = { - /** - * Backbone revisions collection. - * - * Usage: new wp.api.collections.Revisions( {}, { parent: POST_ID } ). - */ - wp.api.collections.Revisions = BaseCollection.extend( - /** @lends Revisions.prototype */ - { - model: wp.api.models.Revision, + /** + * Get a PostsCategories model for an model's categories. + * + * Uses the embedded data if available, otherwises fetches the + * data from the server. + * + * @return {Deferred.promise} promise Resolves to a wp.api.collections.PostsCategories collection containing the post categories. + */ + getCategories: function() { + var postId, embeddeds, categories, + self = this, + classProperties = '', + properties = '', + deferred = jQuery.Deferred(); + + postId = this.get( 'id' ); + embeddeds = this.get( '_embedded' ) || {}; + + // Verify that we have a valied post id. + if ( ! _.isNumber( postId ) ) { + return null; + } + + // If we have embedded categories data, use that when constructing the categories. + if ( embeddeds['https://api.w.org/term'] ) { + properties = embeddeds['https://api.w.org/term'][0]; + } else { + + // Otherwise use the postId. + classProperties = { parent: postId }; + } + + // Create the new categories collection. + categories = new wp.api.collections.PostsCategories( properties, classProperties ); + + // If we didn’t have embedded categories, fetch the categories data. + if ( _.isUndefined( categories.models[0] ) ) { + categories.fetch( { success: function( categories ) { + self.setCategoryPostParents( categories, postId ); + deferred.resolve( categories ); + } } ); + } else { + this.setCategoryPostParents( categories, postId ); + deferred.resolve( categories ); + } + + // Return the constructed categories promise. + return deferred.promise(); + }, + + /** + * Set the category post parents when retrieving posts. + */ + setCategoryPostParents: function( categories, postId ) { + + // Attach post_parent id to the categories. + _.each( categories.models, function( category ) { + category.set( 'parent_post', postId ); + } ); + }, + + /** + * Set the categories for a post. + * + * Accepts an array of category slugs, or a PostsCategories collection. + * + * @param {array|Backbone.Collection} categories The categories to set on the post. + * + */ + setCategories: function( categories ) { + var allCategories, newCategory, + self = this, + newCategories = []; + + // If this is an array of slugs, build a collection. + if ( _.isArray( categories ) ) { + + // Get all the categories. + allCategories = new wp.api.collections.Categories(); + allCategories.fetch( { + success: function( allcats ) { + + // Find the passed categories and set them up. + _.each( categories, function( category ) { + newCategory = new wp.api.models.PostsCategories( allcats.findWhere( { slug: category } ) ); + + // Tie the new category to the post. + newCategory.set( 'parent_post', self.get( 'id' ) ); + + // Add the new category to the collection. + newCategories.push( newCategory ); + } ); + categories = new wp.api.collections.PostsCategories( newCategories ); + self.setCategoriesWithCollection( categories ); + } + } ); + + } else { + this.setCategoriesWithCollection( categories ); + } + + }, + + /** + * Set the categories for a post. + * + * Accepts PostsCategories collection. + * + * @param {array|Backbone.Collection} categories The categories to set on the post. + * + */ + setCategoriesWithCollection: function( categories ) { + var removedCategories, addedCategories, categoriesIds, existingCategoriesIds; + + // Get the existing categories. + this.getCategories().done( function( existingCategories ) { - parent: null, + // Pluck out the category ids. + categoriesIds = categories.pluck( 'id' ); + existingCategoriesIds = existingCategories.pluck( 'id' ); + + // Calculate which categories have been removed or added (leave the rest). + addedCategories = _.difference( categoriesIds, existingCategoriesIds ); + removedCategories = _.difference( existingCategoriesIds, categoriesIds ); + + // Add the added categories. + _.each( addedCategories, function( addedCategory ) { + + // Save the new categories on the post with a 'POST' method, not Backbone's default 'PUT'. + existingCategories.create( categories.get( addedCategory ), { type: 'POST' } ); + } ); + + // Remove the removed categories. + _.each( removedCategories, function( removedCategory ) { + existingCategories.get( removedCategory ).destroy(); + } ); + } ); + } + }, /** - * @class Represent an array of revisions. - * @augments Backbone.Collection. - * @constructs + * Add a helper function to retrieve the author user model. */ - initialize: function( models, options ) { - BaseCollection.prototype.initialize.apply( this, arguments ); + AuthorMixin = { - if ( options && options.parent ) { - this.parent = options.parent; + /** + * Get a user model for an model's author. + * + * Uses the embedded user data if available, otherwises fetches the user + * data from the server. + * + * @return {Object} user A wp.api.models.Users model representing the author user. + */ + getAuthorUser: function() { + var user, authorId, embeddeds, attributes; + + authorId = this.get( 'author' ); + embeddeds = this.get( '_embedded' ) || {}; + + // Verify that we have a valied author id. + if ( ! _.isNumber( authorId ) ) { + return null; + } + + // If we have embedded author data, use that when constructing the user. + if ( embeddeds.author ) { + attributes = _.findWhere( embeddeds.author, { id: authorId } ); + } + + // Otherwise use the authorId. + if ( ! attributes ) { + attributes = { id: authorId }; + } + + // Create the new user model. + user = new wp.api.models.Users( attributes ); + + // If we didn’t have an embedded user, fetch the user data. + if ( ! user.get( 'name' ) ) { + user.fetch(); + } + + // Return the constructed user. + return user; } }, /** - * return URL for collection. - * - * @returns {string}. + * Add a helper function to retrieve the featured image. */ - url: function() { - return WP_API_Settings.root + 'wp/v2/posts/' + this.parent + '/revisions'; + FeaturedImageMixin = { + + /** + * Get a featured image for a post. + * + * Uses the embedded user data if available, otherwises fetches the media + * data from the server. + * + * @return {Object} media A wp.api.models.Media model representing the featured image. + */ + getFeaturedImage: function() { + var media, featuredImageId, embeddeds, attributes; + + featuredImageId = this.get( 'featured_image' ); + embeddeds = this.get( '_embedded' ) || {}; + + // Verify that we have a valid featured image id. + if ( ( ! _.isNumber( featuredImageId ) ) || 0 === featuredImageId ) { + return null; + } + + // If we have embedded featured image data, use that when constructing the user. + if ( embeddeds['https://api.w.org/featuredmedia'] ) { + attributes = _.findWhere( embeddeds['https://api.w.org/featuredmedia'], { id: featuredImageId } ); + } + + // Otherwise use the featuredImageId. + if ( ! attributes ) { + attributes = { id: featuredImageId }; + } + + // Create the new media model. + media = new wp.api.models.Media( attributes ); + + // If we didn’t have an embedded media, fetch the media data. + if ( ! media.get( 'source_url' ) ) { + media.fetch(); + } + + // Return the constructed media. + return media; + } + }; + + // Exit if we don't have valid model defaults. + if ( _.isUndefined( model.defaults ) ) { + return model; + } + + // Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin. + _.each( parseableDates, function( theDateKey ) { + if ( ! _.isUndefined( model.defaults[ theDateKey ] ) ) { + hasDate = true; } + } ); + + // Add the TimeStampedMixin for models that contain a date field. + if ( hasDate ) { + model = model.extend( TimeStampedMixin ); + } + + // Add the AuthorMixin for models that contain an author. + if ( ! _.isUndefined( model.defaults.author ) ) { + model = model.extend( AuthorMixin ); + } + + // Add the FeaturedImageMixin for models that contain a featured_image. + if ( ! _.isUndefined( model.defaults.featured_image ) ) { + model = model.extend( FeaturedImageMixin ); + } + + // Add the CategoriesMixin for models that support categories collections. + if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Categories' ] ) ) { + model = model.extend( CategoriesMixin ); } - ); + + return model; + }; /** - * Todo: Handle schema endpoints. + * Add defaults to a model from a route's endpoints. + * + * @param {array} routeEndpoints Array of route endpoints. + * @param {Object} modelInstance An instance of the model (or collection) + * to add the defaults to. */ + wp.api.decorateFromRoute = function( routeEndpoints, modelInstance ) { + + /** + * Build the defaults based on route endpoint data. + */ + _.each( routeEndpoints, function( routeEndpoint ) { + + // Add post and edit endpoints as model defaults. + if ( _.contains( routeEndpoint.methods, 'POST' ) || _.contains( routeEndpoint.methods, 'PUT' ) ) { + + // Add any non empty args, merging them into the defaults object. + if ( ! _.isEmpty( routeEndpoint.args ) ) { + + // Set as defauls if no defaults yet. + if ( _.isEmpty( modelInstance.defaults ) ) { + modelInstance.defaults = routeEndpoint.args; + } else { + + // We already have defaults, merge these new args in. + modelInstance.defaults = _.union( routeEndpoint.args, modelInstance.defaults ); + } + } + } else { + + // Add GET method as model options. + if ( _.contains( routeEndpoint.methods, 'GET' ) ) { + + // Add any non empty args, merging them into the defaults object. + if ( ! _.isEmpty( routeEndpoint.args ) ) { + + // Set as defauls if no defaults yet. + if ( _.isEmpty( modelInstance.options ) ) { + modelInstance.options = routeEndpoint.args; + } else { + + // We already have options, merge these new args in. + modelInstance.options = _.union( routeEndpoint.args, modelInstance.options ); + } + } + + } + } + + } ); + + /** + * Finish processing the defaults, assigning `defaults` if available, otherwise null. + * + * @todo required arguments + */ + _.each( modelInstance.defaults, function( theDefault, index ) { + if ( _.isUndefined( theDefault['default'] ) ) { + modelInstance.defaults[ index ] = null; + } else { + modelInstance.defaults[ index ] = theDefault['default']; + } + } ); + }; /** - * Todo: Handle post meta. + * Construct the default endpoints and add to an endpoints collection. */ -})( wp, WP_API_Settings, Backbone, _, window ); + // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready. + wp.api.init(); + +})( window ); diff --git a/wp-api.min.js b/wp-api.min.js new file mode 100644 index 0000000000..d5b91c2f5a --- /dev/null +++ b/wp-api.min.js @@ -0,0 +1,2 @@ +!function(a,b){"use strict";function c(){this.models={},this.collections={},this.views={}}a.wp=a.wp||{},wp.api=wp.api||new c,wp.api.versionString=wp.api.versionString||"wp/v2/"}(window),function(a,b){"use strict";var c,d;a.wp=a.wp||{},wp.api=wp.api||{},wp.api.utils=wp.api.utils||{},Date.prototype.toISOString||(c=function(a){return d=String(a),1===d.length&&(d="0"+d),d},Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+c(this.getUTCMonth()+1)+"-"+c(this.getUTCDate())+"T"+c(this.getUTCHours())+":"+c(this.getUTCMinutes())+":"+c(this.getUTCSeconds())+"."+String((this.getUTCMilliseconds()/1e3).toFixed(3)).slice(2,5)+"Z"}),wp.api.utils.parseISO8601=function(a){var c,d,e,f,g=0,h=[1,4,5,6,7,10,11];if(d=/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(a)){for(e=0;f=h[e];++e)d[f]=+d[f]||0;d[2]=(+d[2]||1)-1,d[3]=+d[3]||1,"Z"!==d[8]&&b!==d[9]&&(g=60*d[10]+d[11],"+"===d[9]&&(g=0-g)),c=Date.UTC(d[1],d[2],d[3],d[4],d[5]+g,d[6],d[7])}else c=Date.parse?Date.parse(a):NaN;return c},wp.api.utils.getRootUrl=function(){return a.location.origin?a.location.origin+"/":a.location.protocol+"/"+a.location.host+"/"},wp.api.utils.capitalize=function(a){return _.isUndefined(a)?a:a.charAt(0).toUpperCase()+a.slice(1)},wp.api.utils.extractRoutePart=function(a,b){var c;return b=b||1,a=a.replace(wp.api.versionString,""),c=a.split("/").reverse(),_.isUndefined(c[--b])?"":c[b]},wp.api.utils.extractParentName=function(a){var b,c=a.lastIndexOf("_id>[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage Date: Sat, 9 Jan 2016 14:59:43 -0700 Subject: [PATCH 2/5] Ensure getters always return promise --- wp-api.js | 28 +++++++++++++++++++--------- wp-api.min.js | 2 +- wp-api.min.map | 2 +- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/wp-api.js b/wp-api.js index 4d8382760e..eef2a18f6a 100644 --- a/wp-api.js +++ b/wp-api.js @@ -847,7 +847,7 @@ deferred.resolve( categories ); } - // Return the constructed categories promise. + // Return a promise. return deferred.promise(); }, @@ -955,8 +955,9 @@ * @return {Object} user A wp.api.models.Users model representing the author user. */ getAuthorUser: function() { - var user, authorId, embeddeds, attributes; + var user, authorId, embeddeds, attributes, deferred; + deferred = jQuery.Deferred(); authorId = this.get( 'author' ); embeddeds = this.get( '_embedded' ) || {}; @@ -980,11 +981,15 @@ // If we didn’t have an embedded user, fetch the user data. if ( ! user.get( 'name' ) ) { - user.fetch(); + user.fetch( { success: function( user ) { + deferred.resolve( user ); + } } ); + } else { + deferred.resolve( user ); } - // Return the constructed user. - return user; + // Return a promise. + return deferred.promise(); } }, @@ -1002,8 +1007,9 @@ * @return {Object} media A wp.api.models.Media model representing the featured image. */ getFeaturedImage: function() { - var media, featuredImageId, embeddeds, attributes; + var media, featuredImageId, embeddeds, attributes, deferred; + deferred = jQuery.Deferred(); featuredImageId = this.get( 'featured_image' ); embeddeds = this.get( '_embedded' ) || {}; @@ -1027,11 +1033,15 @@ // If we didn’t have an embedded media, fetch the media data. if ( ! media.get( 'source_url' ) ) { - media.fetch(); + media.fetch( { success: function( media ) { + deferred.resolve( media ); + } } ); + } else { + deferred.resolve( media ); } - // Return the constructed media. - return media; + // Return a promise. + return deferred.promise(); } }; diff --git a/wp-api.min.js b/wp-api.min.js index d5b91c2f5a..248a9115cf 100644 --- a/wp-api.min.js +++ b/wp-api.min.js @@ -1,2 +1,2 @@ -!function(a,b){"use strict";function c(){this.models={},this.collections={},this.views={}}a.wp=a.wp||{},wp.api=wp.api||new c,wp.api.versionString=wp.api.versionString||"wp/v2/"}(window),function(a,b){"use strict";var c,d;a.wp=a.wp||{},wp.api=wp.api||{},wp.api.utils=wp.api.utils||{},Date.prototype.toISOString||(c=function(a){return d=String(a),1===d.length&&(d="0"+d),d},Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+c(this.getUTCMonth()+1)+"-"+c(this.getUTCDate())+"T"+c(this.getUTCHours())+":"+c(this.getUTCMinutes())+":"+c(this.getUTCSeconds())+"."+String((this.getUTCMilliseconds()/1e3).toFixed(3)).slice(2,5)+"Z"}),wp.api.utils.parseISO8601=function(a){var c,d,e,f,g=0,h=[1,4,5,6,7,10,11];if(d=/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(a)){for(e=0;f=h[e];++e)d[f]=+d[f]||0;d[2]=(+d[2]||1)-1,d[3]=+d[3]||1,"Z"!==d[8]&&b!==d[9]&&(g=60*d[10]+d[11],"+"===d[9]&&(g=0-g)),c=Date.UTC(d[1],d[2],d[3],d[4],d[5]+g,d[6],d[7])}else c=Date.parse?Date.parse(a):NaN;return c},wp.api.utils.getRootUrl=function(){return a.location.origin?a.location.origin+"/":a.location.protocol+"/"+a.location.host+"/"},wp.api.utils.capitalize=function(a){return _.isUndefined(a)?a:a.charAt(0).toUpperCase()+a.slice(1)},wp.api.utils.extractRoutePart=function(a,b){var c;return b=b||1,a=a.replace(wp.api.versionString,""),c=a.split("/").reverse(),_.isUndefined(c[--b])?"":c[b]},wp.api.utils.extractParentName=function(a){var b,c=a.lastIndexOf("_id>[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage Date: Sat, 9 Jan 2016 15:39:27 -0700 Subject: [PATCH 3/5] Add remapping for names, see https://github.com/WP-API/client-js/pull/75 --- wp-api.js | 41 ++++++++++++++++++++++++++++++++++++++++- wp-api.min.js | 2 +- wp-api.min.map | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/wp-api.js b/wp-api.js index eef2a18f6a..24bd34ad9d 100644 --- a/wp-api.js +++ b/wp-api.js @@ -475,7 +475,42 @@ }, constructFromSchema: function() { - var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects; + var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects, + + /** + * Set up the model and collection name mapping options. As the schema is built, the + * model and collection names will be adjusted if they are found in the mapping object. + * + * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options. + * + */ + mapping = wpApiSettings.mapping || { + models: { + 'Categories': 'Category', + 'Comments': 'Comment', + 'Pages': 'Page', + 'PagesMeta': 'PageMeta', + 'PagesRevisions': 'PageRevision', + 'Posts': 'Post', + 'PostsCategories': 'PostCategory', + 'PostsRevisions': 'PostRevision', + 'PostsTags': 'PostTag', + 'Schema': 'Schema', + 'Statuses': 'Status', + 'Tags': 'Tag', + 'Taxonomies': 'Taxonomy', + 'Types': 'Type', + 'Users': 'User' + }, + collections: { + 'PagesMeta': 'PageMeta', + 'PagesRevisions': 'PageRevisions', + 'PostsCategories': 'PostCategories', + 'PostsMeta': 'PostMeta', + 'PostsRevisions': 'PostRevisions', + 'PostsTags': 'PostTags' + } + }; /** * Iterate thru the routes, picking up models and collections to build. Builds two arrays, @@ -531,6 +566,7 @@ // If the model has a parent in its route, add that to its class name. if ( '' !== parentName && parentName !== routeName ) { modelClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); + modelClassName = mapping.models[ modelClassName ] || modelClassName; loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { // Function that returns a constructed url based on the parent and id. @@ -576,6 +612,7 @@ // This is a model without a parent in its route modelClassName = wp.api.utils.capitalize( routeName ); + modelClassName = mapping.models[ modelClassName ] || modelClassName; loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { // Function that returns a constructed url based on the id. @@ -619,6 +656,7 @@ if ( '' !== parentName && parentName !== routeName ) { collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); + collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { // Function that returns a constructed url passed on the parent. @@ -644,6 +682,7 @@ // This is a collection without a parent in its route. collectionClassName = wp.api.utils.capitalize( routeName ); + collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { // For the url of a root level collection, use a string. diff --git a/wp-api.min.js b/wp-api.min.js index 248a9115cf..07cecdeb9a 100644 --- a/wp-api.min.js +++ b/wp-api.min.js @@ -1,2 +1,2 @@ -!function(a,b){"use strict";function c(){this.models={},this.collections={},this.views={}}a.wp=a.wp||{},wp.api=wp.api||new c,wp.api.versionString=wp.api.versionString||"wp/v2/"}(window),function(a,b){"use strict";var c,d;a.wp=a.wp||{},wp.api=wp.api||{},wp.api.utils=wp.api.utils||{},Date.prototype.toISOString||(c=function(a){return d=String(a),1===d.length&&(d="0"+d),d},Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+c(this.getUTCMonth()+1)+"-"+c(this.getUTCDate())+"T"+c(this.getUTCHours())+":"+c(this.getUTCMinutes())+":"+c(this.getUTCSeconds())+"."+String((this.getUTCMilliseconds()/1e3).toFixed(3)).slice(2,5)+"Z"}),wp.api.utils.parseISO8601=function(a){var c,d,e,f,g=0,h=[1,4,5,6,7,10,11];if(d=/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(a)){for(e=0;f=h[e];++e)d[f]=+d[f]||0;d[2]=(+d[2]||1)-1,d[3]=+d[3]||1,"Z"!==d[8]&&b!==d[9]&&(g=60*d[10]+d[11],"+"===d[9]&&(g=0-g)),c=Date.UTC(d[1],d[2],d[3],d[4],d[5]+g,d[6],d[7])}else c=Date.parse?Date.parse(a):NaN;return c},wp.api.utils.getRootUrl=function(){return a.location.origin?a.location.origin+"/":a.location.protocol+"/"+a.location.host+"/"},wp.api.utils.capitalize=function(a){return _.isUndefined(a)?a:a.charAt(0).toUpperCase()+a.slice(1)},wp.api.utils.extractRoutePart=function(a,b){var c;return b=b||1,a=a.replace(wp.api.versionString,""),c=a.split("/").reverse(),_.isUndefined(c[--b])?"":c[b]},wp.api.utils.extractParentName=function(a){var b,c=a.lastIndexOf("_id>[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage Date: Sat, 9 Jan 2016 23:32:51 -0700 Subject: [PATCH 4/5] Fixes after name mapping --- wp-api.js | 26 ++++++++++++++------------ wp-api.min.js | 2 +- wp-api.min.map | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/wp-api.js b/wp-api.js index 24bd34ad9d..e18b5bf3e6 100644 --- a/wp-api.js +++ b/wp-api.js @@ -648,7 +648,7 @@ _.each( collectionRoutes, function( collectionRoute ) { // Extract the name and any parent from the route. - var collectionClassName, + var collectionClassName, modelClassName, routeName = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ), parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 3 ); @@ -656,6 +656,7 @@ if ( '' !== parentName && parentName !== routeName ) { collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); + modelClassName = mapping.models[ collectionClassName ] || collectionClassName; collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { @@ -667,7 +668,7 @@ }, // Specify the model that this collection contains. - model: loadingObjects.models[ collectionClassName ], + model: loadingObjects.models[ modelClassName ], // Include a reference to the original class name. name: collectionClassName, @@ -682,6 +683,7 @@ // This is a collection without a parent in its route. collectionClassName = wp.api.utils.capitalize( routeName ); + modelClassName = mapping.models[ collectionClassName ] || collectionClassName; collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { @@ -689,7 +691,7 @@ url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName, // Specify the model that this collection contains. - model: loadingObjects.models[ collectionClassName ], + model: loadingObjects.models[ modelClassName ], // Include a reference to the original class name. name: collectionClassName, @@ -841,12 +843,12 @@ CategoriesMixin = { /** - * Get a PostsCategories model for an model's categories. + * Get a PostCategories model for an model's categories. * * Uses the embedded data if available, otherwises fetches the * data from the server. * - * @return {Deferred.promise} promise Resolves to a wp.api.collections.PostsCategories collection containing the post categories. + * @return {Deferred.promise} promise Resolves to a wp.api.collections.PostCategories collection containing the post categories. */ getCategories: function() { var postId, embeddeds, categories, @@ -873,7 +875,7 @@ } // Create the new categories collection. - categories = new wp.api.collections.PostsCategories( properties, classProperties ); + categories = new wp.api.collections.PostCategories( properties, classProperties ); // If we didn’t have embedded categories, fetch the categories data. if ( _.isUndefined( categories.models[0] ) ) { @@ -904,7 +906,7 @@ /** * Set the categories for a post. * - * Accepts an array of category slugs, or a PostsCategories collection. + * Accepts an array of category slugs, or a PostCategories collection. * * @param {array|Backbone.Collection} categories The categories to set on the post. * @@ -924,7 +926,7 @@ // Find the passed categories and set them up. _.each( categories, function( category ) { - newCategory = new wp.api.models.PostsCategories( allcats.findWhere( { slug: category } ) ); + newCategory = new wp.api.models.PostCategories( allcats.findWhere( { slug: category } ) ); // Tie the new category to the post. newCategory.set( 'parent_post', self.get( 'id' ) ); @@ -932,7 +934,7 @@ // Add the new category to the collection. newCategories.push( newCategory ); } ); - categories = new wp.api.collections.PostsCategories( newCategories ); + categories = new wp.api.collections.PostCategories( newCategories ); self.setCategoriesWithCollection( categories ); } } ); @@ -946,7 +948,7 @@ /** * Set the categories for a post. * - * Accepts PostsCategories collection. + * Accepts PostCategories collection. * * @param {array|Backbone.Collection} categories The categories to set on the post. * @@ -991,7 +993,7 @@ * Uses the embedded user data if available, otherwises fetches the user * data from the server. * - * @return {Object} user A wp.api.models.Users model representing the author user. + * @return {Object} user A wp.api.models.User model representing the author user. */ getAuthorUser: function() { var user, authorId, embeddeds, attributes, deferred; @@ -1016,7 +1018,7 @@ } // Create the new user model. - user = new wp.api.models.Users( attributes ); + user = new wp.api.models.User( attributes ); // If we didn’t have an embedded user, fetch the user data. if ( ! user.get( 'name' ) ) { diff --git a/wp-api.min.js b/wp-api.min.js index 07cecdeb9a..1b147a69d1 100644 --- a/wp-api.min.js +++ b/wp-api.min.js @@ -1,2 +1,2 @@ -!function(a,b){"use strict";function c(){this.models={},this.collections={},this.views={}}a.wp=a.wp||{},wp.api=wp.api||new c,wp.api.versionString=wp.api.versionString||"wp/v2/"}(window),function(a,b){"use strict";var c,d;a.wp=a.wp||{},wp.api=wp.api||{},wp.api.utils=wp.api.utils||{},Date.prototype.toISOString||(c=function(a){return d=String(a),1===d.length&&(d="0"+d),d},Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+c(this.getUTCMonth()+1)+"-"+c(this.getUTCDate())+"T"+c(this.getUTCHours())+":"+c(this.getUTCMinutes())+":"+c(this.getUTCSeconds())+"."+String((this.getUTCMilliseconds()/1e3).toFixed(3)).slice(2,5)+"Z"}),wp.api.utils.parseISO8601=function(a){var c,d,e,f,g=0,h=[1,4,5,6,7,10,11];if(d=/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(a)){for(e=0;f=h[e];++e)d[f]=+d[f]||0;d[2]=(+d[2]||1)-1,d[3]=+d[3]||1,"Z"!==d[8]&&b!==d[9]&&(g=60*d[10]+d[11],"+"===d[9]&&(g=0-g)),c=Date.UTC(d[1],d[2],d[3],d[4],d[5]+g,d[6],d[7])}else c=Date.parse?Date.parse(a):NaN;return c},wp.api.utils.getRootUrl=function(){return a.location.origin?a.location.origin+"/":a.location.protocol+"/"+a.location.host+"/"},wp.api.utils.capitalize=function(a){return _.isUndefined(a)?a:a.charAt(0).toUpperCase()+a.slice(1)},wp.api.utils.extractRoutePart=function(a,b){var c;return b=b||1,a=a.replace(wp.api.versionString,""),c=a.split("/").reverse(),_.isUndefined(c[--b])?"":c[b]},wp.api.utils.extractParentName=function(a){var b,c=a.lastIndexOf("_id>[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage Date: Sun, 10 Jan 2016 10:48:15 -0700 Subject: [PATCH 5/5] DRY helpers, add getMeta, getTags, getRevisions #76 --- wp-api.js | 309 ++++++++++++++++++++++++++++--------------------- wp-api.min.js | 2 +- wp-api.min.map | 2 +- 3 files changed, 178 insertions(+), 135 deletions(-) diff --git a/wp-api.js b/wp-api.js index e18b5bf3e6..3c3eb399bd 100644 --- a/wp-api.js +++ b/wp-api.js @@ -838,69 +838,183 @@ }, /** - * Add a helper funtion to handle post Categories. + * Build a helper function to retrieve related model. + * + * @param {string} parentModel The parent model. + * @param {int} modelId The model ID if the object to request + * @param {string} modelName The model name to use when constructing the model. + * @param {string} embedSourcePoint Where to check the embedds object for _embed data. + * @param {string} embedCheckField Which model field to check to see if the model has data. + * + * @return {Deferred.promise} A promise which resolves to the constructed model. */ - CategoriesMixin = { + buildModelGetter = function( parentModel, modelId, modelName, embedSourcePoint, embedCheckField ) { + var getModel, embeddeds, attributes, deferred; + + deferred = jQuery.Deferred(); + embeddeds = parentModel.get( '_embedded' ) || {}; + + // Verify that we have a valied author id. + if ( ! _.isNumber( modelId ) ) { + deferred.reject(); + return deferred; + } + + // If we have embedded object data, use that when constructing the getModel. + if ( embeddeds[ embedSourcePoint ] ) { + attributes = _.findWhere( embeddeds[ embedSourcePoint ], { id: modelId } ); + } + + // Otherwise use the modelId. + if ( ! attributes ) { + attributes = { id: modelId }; + } + + // Create the new getModel model. + getModel = new wp.api.models[ modelName ]( attributes ); + + // If we didn’t have an embedded getModel, fetch the getModel data. + if ( ! getModel.get( embedCheckField ) ) { + getModel.fetch( { success: function( getModel ) { + deferred.resolve( getModel ); + } } ); + } else { + deferred.resolve( getModel ); + } + // Return a promise. + return deferred.promise(); + }, + + /** + * Build a helper to retrieve a collection. + * + * @param {string} parentModel The parent model. + * @param {string} collectionName The name to use when constructing the collection. + * @param {string} embedSourcePoint Where to check the embedds object for _embed data. + * @param {string} embedIndex An addiitonal optional index for the _embed data. + * + * @return {Deferred.promise} A promise which resolves to the constructed collection. + */ + buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) { /** - * Get a PostCategories model for an model's categories. + * Returns a promise that resolves to the requested collection * * Uses the embedded data if available, otherwises fetches the * data from the server. * - * @return {Deferred.promise} promise Resolves to a wp.api.collections.PostCategories collection containing the post categories. + * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ] + * collection. */ - getCategories: function() { - var postId, embeddeds, categories, - self = this, - classProperties = '', - properties = '', - deferred = jQuery.Deferred(); - - postId = this.get( 'id' ); - embeddeds = this.get( '_embedded' ) || {}; - - // Verify that we have a valied post id. - if ( ! _.isNumber( postId ) ) { - return null; - } + var postId, embeddeds, getObjects, + classProperties = '', + properties = '', + deferred = jQuery.Deferred(); + + postId = parentModel.get( 'id' ); + embeddeds = parentModel.get( '_embedded' ) || {}; + + // Verify that we have a valied post id. + if ( ! _.isNumber( postId ) || 0 === postId ) { + deferred.reject(); + return deferred; + } + + // If we have embedded getObjects data, use that when constructing the getObjects. + if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) { - // If we have embedded categories data, use that when constructing the categories. - if ( embeddeds['https://api.w.org/term'] ) { - properties = embeddeds['https://api.w.org/term'][0]; + // Some embeds also include an index offset, check for that. + if ( _.isUndefined( embedIndex ) ) { + + // Use the embed source point directly. + properties = embeddeds[ embedSourcePoint ]; } else { - // Otherwise use the postId. - classProperties = { parent: postId }; + // Add the index to the embed source point. + properties = embeddeds[ embedSourcePoint ][ embedIndex ]; } + } else { - // Create the new categories collection. - categories = new wp.api.collections.PostCategories( properties, classProperties ); + // Otherwise use the postId. + classProperties = { parent: postId }; + } - // If we didn’t have embedded categories, fetch the categories data. - if ( _.isUndefined( categories.models[0] ) ) { - categories.fetch( { success: function( categories ) { - self.setCategoryPostParents( categories, postId ); - deferred.resolve( categories ); - } } ); - } else { - this.setCategoryPostParents( categories, postId ); - deferred.resolve( categories ); - } + // Create the new getObjects collection. + getObjects = new wp.api.collections[ collectionName ]( properties, classProperties ); - // Return a promise. - return deferred.promise(); - }, + // If we didn’t have embedded getObjects, fetch the getObjects data. + if ( _.isUndefined( getObjects.models[0] ) ) { + getObjects.fetch( { success: function( getObjects ) { + + // Add a helper 'parent_post' attribute onto the model. + setHelperParentPost( getObjects, postId ); + deferred.resolve( getObjects ); + } } ); + } else { + + // Add a helper 'parent_post' attribute onto the model. + setHelperParentPost( getObjects, postId ); + deferred.resolve( getObjects ); + } + + // Return a promise. + return deferred.promise(); + + }, + + /** + * Set the model post parent. + */ + setHelperParentPost = function( collection, postId ) { + + // Attach post_parent id to the collection. + _.each( collection.models, function( model ) { + model.set( 'parent_post', postId ); + } ); + }, + + /** + * Add a helper funtion to handle post Meta. + */ + MetaMixin = { + getMeta: function() { + return buildCollectionGetter( this, 'PostMeta', 'https://api.w.org/meta' ); + } + }, + + /** + * Add a helper funtion to handle post Revisions. + */ + RevisionsMixin = { + getRevisions: function() { + return buildCollectionGetter( this, 'PostRevisions' ); + } + }, + + /** + * Add a helper funtion to handle post Tags. + */ + TagsMixin = { + getTags: function() { + return buildCollectionGetter( this, 'PostTags', 'https://api.w.org/term', 1 ); + } + }, + /** + * Add a helper funtion to handle post Categories. + */ + CategoriesMixin = { /** - * Set the category post parents when retrieving posts. + * Get a PostCategories model for an model's categories. + * + * Uses the embedded data if available, otherwises fetches the + * data from the server. + * + * @return {Deferred.promise} promise Resolves to a wp.api.collections.PostCategories + * collection containing the post categories. */ - setCategoryPostParents: function( categories, postId ) { - - // Attach post_parent id to the categories. - _.each( categories.models, function( category ) { - category.set( 'parent_post', postId ); - } ); + getCategories: function() { + return buildCollectionGetter( this, 'PostCategories', 'https://api.w.org/term', 0 ); }, /** @@ -986,51 +1100,8 @@ * Add a helper function to retrieve the author user model. */ AuthorMixin = { - - /** - * Get a user model for an model's author. - * - * Uses the embedded user data if available, otherwises fetches the user - * data from the server. - * - * @return {Object} user A wp.api.models.User model representing the author user. - */ getAuthorUser: function() { - var user, authorId, embeddeds, attributes, deferred; - - deferred = jQuery.Deferred(); - authorId = this.get( 'author' ); - embeddeds = this.get( '_embedded' ) || {}; - - // Verify that we have a valied author id. - if ( ! _.isNumber( authorId ) ) { - return null; - } - - // If we have embedded author data, use that when constructing the user. - if ( embeddeds.author ) { - attributes = _.findWhere( embeddeds.author, { id: authorId } ); - } - - // Otherwise use the authorId. - if ( ! attributes ) { - attributes = { id: authorId }; - } - - // Create the new user model. - user = new wp.api.models.User( attributes ); - - // If we didn’t have an embedded user, fetch the user data. - if ( ! user.get( 'name' ) ) { - user.fetch( { success: function( user ) { - deferred.resolve( user ); - } } ); - } else { - deferred.resolve( user ); - } - - // Return a promise. - return deferred.promise(); + return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' ); } }, @@ -1038,51 +1109,8 @@ * Add a helper function to retrieve the featured image. */ FeaturedImageMixin = { - - /** - * Get a featured image for a post. - * - * Uses the embedded user data if available, otherwises fetches the media - * data from the server. - * - * @return {Object} media A wp.api.models.Media model representing the featured image. - */ getFeaturedImage: function() { - var media, featuredImageId, embeddeds, attributes, deferred; - - deferred = jQuery.Deferred(); - featuredImageId = this.get( 'featured_image' ); - embeddeds = this.get( '_embedded' ) || {}; - - // Verify that we have a valid featured image id. - if ( ( ! _.isNumber( featuredImageId ) ) || 0 === featuredImageId ) { - return null; - } - - // If we have embedded featured image data, use that when constructing the user. - if ( embeddeds['https://api.w.org/featuredmedia'] ) { - attributes = _.findWhere( embeddeds['https://api.w.org/featuredmedia'], { id: featuredImageId } ); - } - - // Otherwise use the featuredImageId. - if ( ! attributes ) { - attributes = { id: featuredImageId }; - } - - // Create the new media model. - media = new wp.api.models.Media( attributes ); - - // If we didn’t have an embedded media, fetch the media data. - if ( ! media.get( 'source_url' ) ) { - media.fetch( { success: function( media ) { - deferred.resolve( media ); - } } ); - } else { - deferred.resolve( media ); - } - - // Return a promise. - return deferred.promise(); + return buildModelGetter( this, this.get( 'featured_image' ), 'Media', 'https://api.w.org/featuredmedia', 'source_url' ); } }; @@ -1118,6 +1146,21 @@ model = model.extend( CategoriesMixin ); } + // Add the MetaMixin for models that support meta collections. + if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Meta' ] ) ) { + model = model.extend( MetaMixin ); + } + + // Add the TagsMixin for models that support tags collections. + if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Tags' ] ) ) { + model = model.extend( TagsMixin ); + } + + // Add the RevisionsMixin for models that support revisions collections. + if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) { + model = model.extend( RevisionsMixin ); + } + return model; }; diff --git a/wp-api.min.js b/wp-api.min.js index 1b147a69d1..25593b5827 100644 --- a/wp-api.min.js +++ b/wp-api.min.js @@ -1,2 +1,2 @@ -!function(a,b){"use strict";function c(){this.models={},this.collections={},this.views={}}a.wp=a.wp||{},wp.api=wp.api||new c,wp.api.versionString=wp.api.versionString||"wp/v2/"}(window),function(a,b){"use strict";var c,d;a.wp=a.wp||{},wp.api=wp.api||{},wp.api.utils=wp.api.utils||{},Date.prototype.toISOString||(c=function(a){return d=String(a),1===d.length&&(d="0"+d),d},Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+c(this.getUTCMonth()+1)+"-"+c(this.getUTCDate())+"T"+c(this.getUTCHours())+":"+c(this.getUTCMinutes())+":"+c(this.getUTCSeconds())+"."+String((this.getUTCMilliseconds()/1e3).toFixed(3)).slice(2,5)+"Z"}),wp.api.utils.parseISO8601=function(a){var c,d,e,f,g=0,h=[1,4,5,6,7,10,11];if(d=/^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(a)){for(e=0;f=h[e];++e)d[f]=+d[f]||0;d[2]=(+d[2]||1)-1,d[3]=+d[3]||1,"Z"!==d[8]&&b!==d[9]&&(g=60*d[10]+d[11],"+"===d[9]&&(g=0-g)),c=Date.UTC(d[1],d[2],d[3],d[4],d[5]+g,d[6],d[7])}else c=Date.parse?Date.parse(a):NaN;return c},wp.api.utils.getRootUrl=function(){return a.location.origin?a.location.origin+"/":a.location.protocol+"/"+a.location.host+"/"},wp.api.utils.capitalize=function(a){return _.isUndefined(a)?a:a.charAt(0).toUpperCase()+a.slice(1)},wp.api.utils.extractRoutePart=function(a,b){var c;return b=b||1,a=a.replace(wp.api.versionString,""),c=a.split("/").reverse(),_.isUndefined(c[--b])?"":c[b]},wp.api.utils.extractParentName=function(a){var b,c=a.lastIndexOf("_id>[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage[\\d]+)/");return 0>c?"":(b=a.substr(0,c-1),b=b.split("/"),b.pop(),b=b.pop())}}(window),function(a,b,c,d,e){"use strict";a.api.WPApiBaseModel=c.Model.extend({sync:function(a,d,e){var f;return e=e||{},_.isUndefined(b.nonce)||_.isNull(b.nonce)||(f=e.beforeSend,e.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),f?f.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===a&&(d.url=d.url()+"?force=true"),c.sync(a,d,e)},save:function(a,b){return _.contains(this.methods,"PUT")||_.contains(this.methods,"POST")?c.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.contains(this.methods,"DELETE")?c.Model.prototype.destroy.call(this,a):!1}}),a.api.models.Schema=a.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(c,d){var e=this;d=d||{},a.api.WPApiBaseModel.prototype.initialize.call(e,c,d),e.apiRoot=d.apiRoot||b.root,e.versionString=d.versionString||b.versionString},url:function(){return this.apiRoot+this.versionString}})}(wp,wpApiSettings,Backbone,window),function(a,b,c,d,e,f){"use strict";a.api.WPApiBaseCollection=c.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},d.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(a,e,f){var g,h,i=this;return f=f||{},g=f.beforeSend,"undefined"!=typeof b.nonce&&(f.beforeSend=function(a){return a.setRequestHeader("X-WP-Nonce",b.nonce),g?g.apply(i,arguments):void 0}),"read"===a&&(f.data?(i.state.data=d.clone(f.data),delete i.state.data.page):i.state.data=f.data={},"undefined"==typeof f.data.page?(i.state.currentPage=null,i.state.totalPages=null,i.state.totalObjects=null):i.state.currentPage=f.data.page-1,h=f.success,f.success=function(a,b,c){return i.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),i.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10),null===i.state.currentPage?i.state.currentPage=1:i.state.currentPage++,h?h.apply(this,arguments):void 0}),c.sync(a,e,f)},more:function(a){if(a=a||{},a.data=a.data||{},d.extend(a.data,this.state.data),"undefined"==typeof a.data.page){if(!this.hasMore())return!1;null===this.state.currentPage||this.state.currentPage<=1?a.data.page=2:a.data.page=this.state.currentPage+1}return this.fetch(a)},hasMore:function(){return null===this.state.totalPages||null===this.state.totalObjects||null===this.state.currentPage?null:this.state.currentPage