diff --git a/client-js.php b/client-js.php index 1d6df3c..213d200 100644 --- a/client-js.php +++ b/client-js.php @@ -13,7 +13,32 @@ function json_api_client_js() { wp_register_script( 'wp-api', $src, array( 'jquery', 'underscore', 'backbone' ), '1.0', true ); } - $settings = array( 'root' => esc_url_raw( get_rest_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ) ); + /** + * @var \WP_REST_Server $wp_rest_server + */ + global $wp_rest_server; + if ( empty( $wp_rest_server ) ) { + /** This filter is documented in wp-includes/rest-api.php */ + $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' ); + $wp_rest_server = new $wp_rest_server_class(); + + /** This filter is documented in wp-includes/rest-api.php */ + do_action( 'rest_api_init', $wp_rest_server ); + } + + $schema_request = new WP_REST_Request( 'GET', '/wp/v2' ); + $schema_response = $wp_rest_server->dispatch( $schema_request ); + $schema = null; + if ( ! $schema_response->is_error() ) { + $schema = $schema_response->get_data(); + } + + $settings = array( + 'root' => esc_url_raw( get_rest_url() ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'versionString' => 'wp/v2/', + 'schema' => $schema, + ); wp_localize_script( 'wp-api', 'wpApiSettings', $settings ); } diff --git a/js/load.js b/js/load.js index af42dc4..2b38d23 100644 --- a/js/load.js +++ b/js/load.js @@ -3,237 +3,275 @@ 'use strict'; - var endpointLoading; + var Endpoint, initializedDeferreds = {}; window.wp = window.wp || {}; wp.api = wp.api || {}; - /** - * Initialize the wp-api, optionally passing the API root. - * - * @param {string} apiRoot The api root. Optional, defaults to wpApiSettings.root. - */ - wp.api.init = function( apiRoot, versionString ) { - var schemaModel, - apiConstructor; + Endpoint = Backbone.Model.extend({ + defaults: { + apiRoot: wpApiSettings.root, + versionString: wp.api.versionString, + schema: null, + models: {}, + collections: {} + }, - wp.api.apiRoot = apiRoot || wpApiSettings.root; - wp.api.versionString = versionString || wp.api.versionString; - wpApiSettings.root = wp.api.apiRoot; + initialize: function() { + var model = this, deferred; - /** - * Construct and fetch the API schema. - * - * Use a session Storage cached version if available. - */ - apiConstructor = new jQuery.Deferred(); + Backbone.Model.prototype.initialize.apply( model, arguments ); - // Used a cached copy of the schema model if available. - if ( ! _.isUndefined( sessionStorage ) && sessionStorage.getItem( 'wp-api-schema-model' + apiRoot ) ) { + deferred = jQuery.Deferred(); + model.schemaConstructed = deferred.promise(); - // Grab the schema model from the sessionStorage cache. - schemaModel = new wp.api.models.Schema( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + apiRoot ) ) ); + model.schemaModel = new wp.api.models.Schema( null, { + apiRoot: model.get( 'apiRoot' ), + versionString: model.get( 'versionString' ) + }); - // Contruct the models and collections from the Schema model. - wp.api.constructFromSchema( schemaModel, apiConstructor ); - } else { + model.schemaModel.once( 'change', function() { + model.constructFromSchema(); + deferred.resolve( model ); + } ); - // Construct a new Schema model. - schemaModel = new wp.api.models.Schema(), + if ( model.get( 'schema' ) ) { - // Fetch the schema information from the API. - 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 ) { + // 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' ) ) ) { - // Store a copy of the schema model in the session cache if available. - if ( ! _.isUndefined( sessionStorage ) ) { - sessionStorage.setItem( 'wp-api-schema-model' + apiRoot, JSON.stringify( newSchemaModel ) ); + // 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.toJSON() ) ); + } + }, + + // @todo Handle the error condition. + error: function() { } + }); + } + }, + + constructFromSchema: function() { + var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects; + + /** + * 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 = {}; - // Contruct the models and collections from the Schema model. - wp.api.constructFromSchema( newSchemaModel, apiConstructor ); - }, + /** + * 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 { - // @todo Handle the error condition. - error: function() { + // Collections end in a name. + if ( ! index.endsWith( 'me' ) ) { + collectionRoutes.push( { index: index, route: route } ); + } + } } } ); - } - - return apiConstructor.promise(); - }; - - /** - * Construct the models and collections from the Schema model. - * - * @param {wp.api.models.Schema} Backbone model of the API schema. - * @param {jQuery.Deferred.promise} A promise to send api load updates. - */ - wp.api.constructFromSchema = function( model, apiConstructor ) { - /** - * Iterate thru the routes, picking up models and collections to build. Builds two arrays, - * one for models and one for collections. - */ - var modelRoutes = [], - collectionRoutes = [], - schemaRoot = wp.api.apiRoot.replace( wp.api.utils.getRootUrl(), '' ), - loadingObjects = {}; - - /** - * Tracking objects for models and collections. - */ - loadingObjects.models = {}; - loadingObjects.collections = {}; - - _.each( model.get( 'routes' ), function( route, index ) { - - // Skip the schema root if included in the schema. - if ( index !== wp.api.versionString && - index !== schemaRoot && - index !== ( '/' + wp.api.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 } ); + /** + * 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 + '/' + 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 the array of route methods for easy reference. + methods: modelRoute.route.methods + } ); } else { - // Collections end in a name. - if ( ! index.endsWith( 'me' ) ) { - collectionRoutes.push( { index: index, route: route } ); - } + // This is a model without a parent in its route + modelClassName = wp.api.utils.capitalize( routeName ); + loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { + + // 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; + }, + + // Include a reference to the original route object. + route: modelRoute, + + // Include the array of route methods for easy reference. + methods: modelRoute.route.methods + } ); } - } - } ); - /** - * 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 = wp.api.apiRoot + wp.api.versionString + - parentName + '/' + this.get( 'parent' ) + '/' + - routeName; - if ( ! _.isUndefined( this.get( 'id' ) ) ) { - url += '/' + this.get( 'id' ); - } - return url; - }, + // Add defaults to the new model, pulled form the endpoint + wp.api.decorateFromRoute( modelRoute.route.endpoints, loadingObjects.models[ modelClassName ] ); - // Incldue a refence to the original route object. - route: modelRoute, + // @todo add + } ); - // Include the array of route methods for easy reference. - methods: modelRoute.route.methods - } ); - } else { + /** + * Construct the collections. + * + * Base the class name on the route endpoint. + */ + _.each( collectionRoutes, function( collectionRoute ) { - // This is a model without a parent in its route - modelClassName = wp.api.utils.capitalize( routeName ); - loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { + // 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, 4 ); - // Function that returns a constructed url based on the id. - url: function() { - var url = wp.api.apiRoot + wp.api.versionString + routeName; - if ( ! _.isUndefined( this.get( 'id' ) ) ) { - url += '/' + this.get( 'id' ); - } - return url; - }, + // If the collection has a parent in its route, add that to its class name/ + if ( '' !== parentName && parentName !== routeName ) { - // Incldue a refence to the original route object. - route: modelRoute, + collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); + loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { - // Include the array of route methods for easy reference. - methods: modelRoute.route.methods - } ); - } + // Function that returns a constructed url passed on the parent. + url: function() { + return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + + parentName + '/' + this.parent + '/' + + routeName; + }, - // Add defaults to the new model, pulled form the endpoint - wp.api.decorateFromRoute( modelRoute.route.endpoints, loadingObjects.models[ modelClassName ] ); + // Specify the model that this collection contains. + model: loadingObjects.models[ collectionClassName ], - // @todo add - } ); + // Include a reference to the original route object. + route: collectionRoute, - /** - * Construct the collections. - * - * Base the class name on the route endpoint. - */ - _.each( collectionRoutes, function( collectionRoute ) { + // Include the array of route methods for easy reference. + methods: collectionRoute.route.methods + } ); + } else { - // 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, 4 ); + // This is a collection without a parent in its route. + collectionClassName = wp.api.utils.capitalize( routeName ); + loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { - // If the collection has a parent in its route, add that to its class name/ - if ( '' !== parentName && parentName !== routeName ) { + // For the url of a root level collection, use a string. + url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName, - collectionClassName = wp.api.utils.capitalize( parentName ) + wp.api.utils.capitalize( routeName ); - loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { + // Specify the model that this collection contains. + model: loadingObjects.models[ collectionClassName ], - // Function that returns a constructed url pased on the parent. - url: function() { - return wp.api.apiRoot + wp.api.versionString + - parentName + '/' + this.parent + '/' + - routeName; - }, - model: loadingObjects.models[collectionClassName], + // Include a reference to the original route object. + route: collectionRoute, - // Incldue a refence to the original route object. - route: collectionRoute, + // Include the array of route methods for easy reference. + methods: collectionRoute.route.methods + } ); + } - // Include the array of route methods for easy reference. - methods: collectionRoute.route.methods - } ); - } else { + // Add defaults to the new model, pulled form the endpoint + wp.api.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] ); + } ); + } - // 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: wp.api.apiRoot + wp.api.versionString + routeName, + wp.api.endpoints = new Backbone.Collection({ + model: Endpoint + }); - // Incldue a refence to the original route object. - route: collectionRoute, + /** + * 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; + } - // Include the array of route methods for easy reference. - methods: collectionRoute.route.methods - } ); + 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 ); } - - // Add defaults to the new model, pulled form the endpoint - wp.api.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] ); - } ); - - _.defer( function() { - apiConstructor.resolve( loadingObjects ); - } ); + 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 ]; }; /** @@ -305,20 +343,8 @@ /** * Construct the default endpoints and add to an endpoints collection. */ - wp.api.endpoints = new Backbone.Collection(); // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready. - endpointLoading = wp.api.init(); - - // When the endpoint is loaded, complete the setup process. - endpointLoading.done( function( endpoint ) { - - // Map the default endpoints, extending any already present items (including Schema model). - wp.api.models = _.extend( endpoint.models, wp.api.models ); - wp.api.collections = _.extend( endpoint.collections, wp.api.collections ); - - // Add the endpoint to the endpoints collection. - wp.api.endpoints.push( endpoint ); - } ); + wp.api.init(); })( window ); diff --git a/js/models.js b/js/models.js index 38e69c2..629bf5d 100644 --- a/js/models.js +++ b/js/models.js @@ -191,10 +191,26 @@ * API Schema model. Contains meta information about the API. */ wp.api.models.Schema = wp.api.WPApiBaseModel.extend( - /** @lends Shema.prototype */ + /** @lends Schema.prototype */ { + defaults: { + _links: {}, + namespace: null, + routes: {} + }, + + 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; + }, + url: function() { - return wpApiSettings.root + wp.api.versionString; + return this.apiRoot + this.versionString; } } );