From 43f66a5423785e25e75e517496fd7f6242b941b9 Mon Sep 17 00:00:00 2001 From: Taylor Lovett Date: Fri, 19 Dec 2014 17:14:52 -0500 Subject: [PATCH] Merge JS into main repo --- plugin.php | 2 +- wp-api.js | 987 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 wp-api.js diff --git a/plugin.php b/plugin.php index a22f8f6ad2..b56a8eacba 100644 --- a/plugin.php +++ b/plugin.php @@ -238,7 +238,7 @@ function json_api_deactivation( $network_wide ) { * @see wp_register_scripts() */ function json_register_scripts() { - wp_register_script( 'wp-api', 'http://wp-api.github.io/client-js/build/js/wp-api.js', array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); + wp_register_script( 'wp-api', esc_url_raw( plugins_url( 'wp-api.js', __FILE__ ) ), array( 'jquery', 'backbone', 'underscore' ), '1.1', true ); $settings = array( 'root' => esc_url_raw( get_json_url() ), 'nonce' => wp_create_nonce( 'wp_json' ) ); wp_localize_script( 'wp-api', 'WP_API_Settings', $settings ); diff --git a/wp-api.js b/wp-api.js new file mode 100644 index 0000000000..56e70c2bf2 --- /dev/null +++ b/wp-api.js @@ -0,0 +1,987 @@ +(function( window, undefined ) { + + 'use strict'; + + function WP_API() { + this.models = {}; + this.collections = {}; + this.views = {}; + } + + window.wp = window.wp || {}; + wp.api = wp.api || new WP_API(); + +})( window ); + +(function( Backbone, _, window, undefined ) { + + //'use strict'; + + // ECMAScript 5 shim, from MDN + // 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 ) { + r = '0' + r; + } + return r; + }; + + Date.prototype.toISOString = function() { + return this.getUTCFullYear() + + '-' + pad( this.getUTCMonth() + 1 ) + + '-' + pad( this.getUTCDate() ) + + 'T' + pad( this.getUTCHours() ) + + ':' + pad( this.getUTCMinutes() ) + + ':' + pad( this.getUTCSeconds() ) + + '.' + String( ( this.getUTCMilliseconds()/1000 ).toFixed( 3 ) ).slice( 2, 5 ) + + 'Z'; + }; + } + + function WP_API_Utils() { + var origParse = Date.parse, + numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; + + + this.parseISO8601 = function( date ) { + var timestamp, struct, i, k, + minutesOffset = 0; + + // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string + // before falling back to any implementation-specific date parsing, so that’s what we do, even if native + // 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; + } + + // allow undefined days and months + struct[2] = ( +struct[2] || 1 ) - 1; + struct[3] = +struct[3] || 1; + + if ( struct[8] !== 'Z' && struct[9] !== undefined ) { + minutesOffset = struct[10] * 60 + struct[11]; + + if ( struct[9] === '+' ) { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] ); + } + else { + timestamp = origParse ? origParse( date ) : NaN; + } + + return timestamp; + }; + } + + window.wp = window.wp || {}; + wp.api = wp.api || {}; + wp.api.utils = wp.api.utils || new WP_API_Utils(); + +})( Backbone, _, 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'; + + /** + * Array of parseable dates + * + * @type {string[]} + */ + var parseable_dates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ]; + + /** + * Mixin for all content that is time stamped + * + * @type {{toJSON: toJSON, parse: parse}} + */ + 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 ); + }); + + // Parse the author into a User object + if ( response.author !== 'undefined' ) { + response.author = new wp.api.models.User( response.author ); + } + + return response; + } + }; + + /** + * Mixin for all hierarchical content types such as posts + * + * @type {{parent: parent}} + */ + var HierarchicalMixin = { + /** + * Get parent object + * + * @returns {Backbone.Model} + */ + parent: function() { + + var object, parent = this.get( 'parent' ); + + // Return null if we don't have a parent + if ( parent === 0 ) { + return null; + } + + var parentModel = this; + + if ( typeof this.parentModel !== 'undefined' ) { + /** + * Probably a better way to do this. Perhaps grab a cached version of the + * instantiated model? + */ + parentModel = new this.parentModel(); + } + + // Can we get this from its collection? + if ( parentModel.collection ) { + return parentModel.collection.get( parent ); + } else { + // Otherwise, get the object directly + object = new parentModel.constructor( { + ID: parent + }); + + // Note that this acts asynchronously + object.fetch(); + return object; + } + } + }; + + /** + * Private Backbone base model for all models + */ + var BaseModel = Backbone.Model.extend( + /** @lends BaseModel.prototype */ + { + /** + * Set nonce header before every Backbone sync + * + * @param {string} method + * @param {Backbone.Model} model + * @param {{beforeSend}, *} options + * @returns {*} + */ + sync: function( method, model, options ) { + options = options || {}; + + if ( typeof WP_API_Settings.nonce !== 'undefined' ) { + var beforeSend = options.beforeSend; + + options.beforeSend = function( xhr ) { + xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce ); + + if ( beforeSend ) { + return beforeSend.apply( this, arguments ); + } + }; + } + + return Backbone.sync( method, model, options ); + } + } + ); + + /** + * Backbone model for single users + */ + wp.api.models.User = BaseModel.extend( + /** @lends User.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/users', + + defaults: { + ID: null, + username: '', + email: '', + password: '', + name: '', + first_name: '', + last_name: '', + nickname: '', + slug: '', + URL: '', + avatar: '', + meta: { + links: {} + } + }, + + /** + * Return avatar URL + * + * @param {number} size + * @returns {string} + */ + avatar: function( size ) { + return this.get( 'avatar' ) + '&s=' + size; + } + } + ); + + /** + * Model for Taxonomy + */ + wp.api.models.Taxonomy = BaseModel.extend( + /** @lends Taxonomy.prototype */ + { + idAttribute: 'slug', + + urlRoot: WP_API_Settings.root + '/taxonomies', + + defaults: { + name: '', + slug: null, + labels: {}, + types: {}, + show_cloud: false, + hierarchical: false, + meta: { + links: {} + } + } + } + ); + + /** + * Backbone model for term + */ + wp.api.models.Term = BaseModel.extend( _.extend( + /** @lends Term.prototype */ + { + idAttribute: 'ID', + + taxonomy: 'category', + + /** + * @class Represent a term + * @augments Backbone.Model + * @constructs + */ + initialize: function( attributes, options ) { + if ( typeof options !== 'undefined' ) { + if ( options.taxonomy ) { + this.taxonomy = options.taxonomy; + } + } + }, + + /** + * Return URL for the model + * + * @returns {string} + */ + url: function() { + var id = this.get( 'ID' ); + id = id || ''; + + return WP_API_Settings.root + '/taxonomies/' + this.taxonomy + '/terms/' + id; + }, + + defaults: { + ID: null, + name: '', + slug: '', + description: '', + parent: null, + count: 0, + link: '', + meta: { + links: {} + } + } + + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for single posts + */ + wp.api.models.Post = BaseModel.extend( _.extend( + /** @lends Post.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/posts', + + defaults: { + ID: null, + title: '', + status: 'draft', + type: 'post', + author: new wp.api.models.User(), + content: '', + link: '', + 'parent': 0, + date: new Date(), + date_gmt: new Date(), + modified: new Date(), + modified_gmt: new Date(), + format: 'standard', + slug: '', + guid: '', + excerpt: '', + menu_order: 0, + comment_status: 'open', + ping_status: 'open', + sticky: false, + date_tz: 'Etc/UTC', + modified_tz: 'Etc/UTC', + featured_image: null, + terms: {}, + post_meta: {}, + meta: { + links: {} + } + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for pages + */ + wp.api.models.Page = BaseModel.extend( _.extend( + /** @lends Page.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/pages', + + defaults: { + ID: null, + title: '', + status: 'draft', + type: 'page', + author: new wp.api.models.User(), + content: '', + parent: 0, + link: '', + date: new Date(), + modified: new Date(), + date_gmt: new Date(), + modified_gmt: new Date(), + date_tz: 'Etc/UTC', + modified_tz: 'Etc/UTC', + format: 'standard', + slug: '', + guid: '', + excerpt: '', + menu_order: 0, + comment_status: 'closed', + ping_status: 'open', + sticky: false, + password: '', + meta: { + links: {} + }, + featured_image: null, + terms: [] + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for revisions + */ + wp.api.models.Revision = wp.api.models.Post.extend( + /** @lends Revision.prototype */ + { + /** + * Return URL for model + * + * @returns {string} + */ + url: function() { + var parent_id = this.get( 'parent' ); + parent_id = parent_id || ''; + + var id = this.get( 'ID' ); + id = id || ''; + + return WP_API_Settings.root + '/posts/' + parent_id + '/revisions/' + id; + }, + + /** + * @class Represent a revision + * @augments Backbone.Model + * @constructs + */ + initialize: function() { + // Todo: what of the parent model is a page? + this.parentModel = wp.api.models.Post; + } + } + ); + + /** + * Backbone model for media items + */ + wp.api.models.Media = BaseModel.extend( _.extend( + /** @lends Media.prototype */ + { + idAttribute: 'ID', + + urlRoot: WP_API_Settings.root + '/media', + + defaults: { + ID: null, + title: '', + status: 'inherit', + type: 'attachment', + author: new wp.api.models.User(), + content: '', + parent: 0, + link: '', + date: new Date(), + modified: new Date(), + format: 'standard', + slug: '', + guid: '', + excerpt: '', + menu_order: 0, + comment_status: 'open', + ping_status: 'open', + sticky: false, + date_tz: 'Etc/UTC', + modified_tz: 'Etc/UTC', + date_gmt: new Date(), + modified_gmt: new Date(), + meta: { + links: {} + }, + terms: [], + source: '', + is_image: true, + attachment_meta: {}, + image_meta: {} + }, + + /** + * @class Represent a media item + * @augments Backbone.Model + * @constructs + */ + initialize: function() { + // Todo: what of the parent model is a page? + this.parentModel = wp.api.models.Post; + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for comments + */ + wp.api.models.Comment = BaseModel.extend( _.extend( + /** @lends Comment.prototype */ + { + idAttribute: 'ID', + + defaults: { + ID: null, + post: null, + content: '', + status: 'hold', + type: '', + parent: 0, + author: new wp.api.models.User(), + date: new Date(), + date_gmt: new Date(), + date_tz: 'Etc/UTC', + meta: { + links: {} + } + }, + + /** + * Return URL for model + * + * @returns {string} + */ + url: function() { + var post_id = this.get( 'post' ); + post_id = post_id || ''; + + var id = this.get( 'ID' ); + id = id || ''; + + return WP_API_Settings.root + '/posts/' + post_id + '/comments/' + id; + } + }, TimeStampedMixin, HierarchicalMixin ) + ); + + /** + * Backbone model for single post types + */ + wp.api.models.PostType = BaseModel.extend( + /** @lends PostType.prototype */ + { + idAttribute: 'slug', + + urlRoot: WP_API_Settings.root + '/posts/types', + + defaults: { + slug: null, + name: '', + description: '', + labels: {}, + queryable: false, + searchable: false, + hierarchical: false, + meta: { + links: {} + }, + taxonomies: [] + }, + + /** + * Prevent model from being saved + * + * @returns {boolean} + */ + save: function () { + return false; + }, + + /** + * Prevent model from being deleted + * + * @returns {boolean} + */ + 'delete': function () { + return false; + } + } + ); + + /** + * Backbone model for a post status + */ + wp.api.models.PostStatus = BaseModel.extend( + /** @lends PostStatus.prototype */ + { + idAttribute: 'slug', + + urlRoot: WP_API_Settings.root + '/posts/statuses', + + defaults: { + slug: null, + name: '', + 'public': true, + 'protected': false, + 'private': false, + queryable: true, + show_in_list: true, + meta: { + links: {} + } + }, + + /** + * Prevent model from being saved + * + * @returns {boolean} + */ + save: function() { + return false; + }, + + /** + * Prevent model from being deleted + * + * @returns {boolean} + */ + 'delete': function() { + return false; + } + } + ); + +})( wp, WP_API_Settings, Backbone, _, window ); + +/* global WP_API_Settings:false */ +(function( wp, WP_API_Settings, Backbone, _, window, undefined ) { + + 'use strict'; + + var BaseCollection = Backbone.Collection.extend( + /** @lends BaseCollection.prototype */ + { + + /** + * Setup default state + */ + initialize: function() { + this.state = { + data: {}, + currentPage: null, + totalPages: null, + totalObjects: null + }; + }, + + /** + * Overwrite Backbone.Collection.sync to pagination state based on response headers. + * + * Set nonce header before every Backbone sync. + * + * @param {string} method + * @param {Backbone.Model} model + * @param {{success}, *} options + * @returns {*} + */ + sync: function( method, model, options ) { + options = options || {}; + var beforeSend = options.beforeSend; + + if ( typeof WP_API_Settings.nonce !== 'undefined' ) { + options.beforeSend = function( xhr ) { + xhr.setRequestHeader( 'X-WP-Nonce', WP_API_Settings.nonce ); + + if ( beforeSend ) { + return beforeSend.apply( this, arguments ); + } + }; + } + + if ( 'read' === method ) { + var SELF = this; + + if ( options.data ) { + SELF.state.data = _.clone( options.data ); + + delete SELF.state.data.page; + } else { + SELF.state.data = options.data = {}; + } + + if ( typeof options.data.page === 'undefined' ) { + SELF.state.currentPage = null; + SELF.state.totalPages = null; + SELF.state.totalObjects = null; + } else { + SELF.state.currentPage = options.data.page - 1; + } + + var 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 ) { + SELF.state.currentPage = 1; + } else { + SELF.state.currentPage++; + } + + if ( success ) { + return success.apply( this, arguments ); + } + }; + } + + return Backbone.sync( method, model, options ); + }, + + /** + * Fetches the next page of objects if a new page exists + * + * @param {data: {page}} options + * @returns {*} + */ + more: function( options ) { + options = options || {}; + options.data = options.data || {}; + + _.extend( options.data, this.state.data ); + + if ( typeof options.data.page === 'undefined' ) { + if ( ! this.hasMore() ) { + return false; + } + + if ( this.state.currentPage === null || this.state.currentPage <= 1 ) { + options.data.page = 2; + } else { + options.data.page = this.state.currentPage + 1; + } + } + + return this.fetch( options ); + }, + + /** + * Returns true if there are more pages of objects available + * + * @returns null|boolean + */ + hasMore: function() { + if ( this.state.totalPages === null || + this.state.totalObjects === null || + this.state.currentPage === null ) { + return null; + } else { + return ( this.state.currentPage < this.state.totalPages ); + } + } + } + ); + + /** + * Backbone collection for posts + */ + wp.api.collections.Posts = BaseCollection.extend( + /** @lends Posts.prototype */ + { + url: WP_API_Settings.root + '/posts', + + model: wp.api.models.Post + } + ); + + /** + * Backbone collection for pages + */ + wp.api.collections.Pages = BaseCollection.extend( + /** @lends Pages.prototype */ + { + url: WP_API_Settings.root + '/pages', + + model: wp.api.models.Page + } + ); + + /** + * Backbone users collection + */ + wp.api.collections.Users = BaseCollection.extend( + /** @lends Users.prototype */ + { + url: WP_API_Settings.root + '/users', + + model: wp.api.models.User + } + ); + + /** + * Backbone post statuses collection + */ + wp.api.collections.PostStatuses = BaseCollection.extend( + /** @lends PostStatuses.prototype */ + { + url: WP_API_Settings.root + '/posts/statuses', + + model: wp.api.models.PostStatus + + } + ); + + /** + * Backbone media library collection + */ + wp.api.collections.MediaLibrary = BaseCollection.extend( + /** @lends MediaLibrary.prototype */ + { + url: WP_API_Settings.root + '/media', + + model: wp.api.models.Media + } + ); + + /** + * Backbone taxonomy collection + */ + wp.api.collections.Taxonomies = BaseCollection.extend( + /** @lends Taxonomies.prototype */ + { + model: wp.api.models.Taxonomy, + + url: WP_API_Settings.root + '/taxonomies' + } + ); + + /** + * Backbone comment collection + */ + wp.api.collections.Comments = BaseCollection.extend( + /** @lends Comments.prototype */ + { + model: wp.api.models.Comment, + + post: null, + + /** + * @class Represent an array of comments + * @augments Backbone.Collection + * @constructs + */ + initialize: function( models, options ) { + this.constructor.__super__.initialize.apply( this, arguments ); + + if ( options && options.post ) { + this.post = options.post; + } + }, + + /** + * Return URL for collection + * + * @returns {string} + */ + url: function() { + return WP_API_Settings.root + '/posts/' + this.post + '/comments'; + } + } + ); + + /** + * Backbone post type collection + */ + wp.api.collections.PostTypes = BaseCollection.extend( + /** @lends PostTypes.prototype */ + { + model: wp.api.models.PostType, + + url: WP_API_Settings.root + '/posts/types' + } + ); + + /** + * Backbone terms collection + */ + wp.api.collections.Terms = BaseCollection.extend( + /** @lends Terms.prototype */ + { + model: wp.api.models.Term, + + type: 'post', + + taxonomy: 'category', + + /** + * @class Represent an array of terms + * @augments Backbone.Collection + * @constructs + */ + initialize: function( models, options ) { + this.constructor.__super__.initialize.apply( this, arguments ); + + if ( typeof options !== 'undefined' ) { + if ( options.type ) { + this.type = options.type; + } + + if ( options.taxonomy ) { + this.taxonomy = options.taxonomy; + } + } + + this.on( 'add', _.bind( this.addModel, this ) ); + }, + + /** + * We need to set the type and taxonomy for each model + * + * @param {Backbone.model} model + */ + addModel: function( model ) { + model.type = this.type; + model.taxonomy = this.taxonomy; + }, + + /** + * Return URL for collection + * + * @returns {string} + */ + url: function() { + return WP_API_Settings.root + '/posts/types/' + this.type + '/taxonomies/' + this.taxonomy + '/terms/'; + } + } + ); + + /** + * Backbone revisions collection + */ + wp.api.collections.Revisions = BaseCollection.extend( + /** @lends Revisions.prototype */ + { + model: wp.api.models.Revision, + + parent: null, + + /** + * @class Represent an array of revisions + * @augments Backbone.Collection + * @constructs + */ + initialize: function( models, options ) { + this.constructor.__super__.initialize.apply( this, arguments ); + + if ( options && options.parent ) { + this.parent = options.parent; + } + }, + + /** + * return URL for collection + * + * @returns {string} + */ + url: function() { + return WP_API_Settings.root + '/posts/' + this.parent + '/revisions'; + } + } + ); + +})( wp, WP_API_Settings, Backbone, _, window );