├── .gitignore ├── .gitmodules ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── Contributing.md ├── Gruntfile.js ├── README.md ├── bin └── install-wp-tests.sh ├── bower.json ├── build └── js │ ├── wp-api.js │ ├── wp-api.min.js │ └── wp-api.min.js.map ├── client-js.php ├── js ├── app.js ├── collections.js ├── load.js ├── models.js └── utils.js ├── package.json ├── routes-to-add.md └── tests ├── .jshintrc ├── run-tests.js ├── tests.html └── wp-api.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .sass-cache 3 | bower_components -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dev-lib"] 2 | path = dev-lib 3 | url = https://github.com/xwp/wp-dev-lib.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "wordpress", 3 | "excludeFiles": [ 4 | "**/*.min.js", 5 | "**/node_modules/**", 6 | "**/vendor/**", 7 | "**/bower_components/**" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | **/*.min.js 2 | **/node_modules/** 3 | **/vendor/** 4 | **/bower_components/** 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "eqnull": true, 6 | "es3": true, 7 | "expr": true, 8 | "immed": true, 9 | "noarg": true, 10 | "onevar": true, 11 | "quotmark": "single", 12 | "trailing": true, 13 | "undef": true, 14 | "unused": true, 15 | "browser": true, 16 | 17 | "globals": { 18 | "_": false, 19 | "Backbone": false, 20 | "jQuery": false, 21 | "JSON" : false, 22 | "wp": false, 23 | "console": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: 4 | - php 5 | - node_js 6 | 7 | php: 8 | - 5.6 9 | 10 | node_js: 11 | - 0.10 12 | 13 | env: 14 | - WP_VERSION=latest WP_MULTISITE=0 WP_PROJECT_TYPE=plugin WP_TEST_URL=http://localhost:80 15 | 16 | before_script: 17 | - export DEV_LIB_PATH=dev-lib 18 | - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi 19 | - source $DEV_LIB_PATH/travis.before_script.sh 20 | - wp plugin install WP-API 21 | - wp plugin activate WP-API 22 | - npm install 23 | 24 | script: 25 | - $DEV_LIB_PATH/travis.script.sh 26 | - grunt 27 | 28 | after_script: 29 | - $DEV_LIB_PATH/travis.after_script.sh 30 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Hi, and thanks for considering contributing! Before you do though, here's a few 3 | notes on how best to contribute. 4 | 5 | ## Pull Request Process 6 | Changes are proposed in the form of pull requests against `master` by you, the contributor! 7 | 8 | ## Best Practices 9 | 10 | ### Commit Messages 11 | Commit messages should follow the standard laid out in the git manual; that is, 12 | a one-line summary () 13 | 14 | Short (50 chars or less) summary of changes 15 | 16 | More detailed explanatory text, if necessary. Wrap it to about 72 17 | characters or so. In some contexts, the first line is treated as the 18 | subject of an email and the rest of the text as the body. The blank 19 | line separating the summary from the body is critical (unless you omit 20 | the body entirely); tools like rebase can get confused if you run the 21 | two together. 22 | 23 | 24 | 25 | ## Develolpment Workflow 26 | 27 | New features and bug fixes are developed in branches, and merged to `develop` for testing. 28 | 29 | Tested features are merged into `master`. Pull requests should be made against `master` 30 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | module.exports = function( grunt ) { 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON( 'package.json' ), 5 | jshint: { 6 | options: grunt.file.readJSON( '.jshintrc' ), 7 | grunt: { 8 | src: [ 'Gruntfile.js' ] 9 | }, 10 | tests: { 11 | src: [ 12 | 'tests/**/*.js' 13 | ], 14 | options: grunt.file.readJSON( 'tests/.jshintrc' ) 15 | }, 16 | core: { 17 | src: [ 18 | 'js/*.js' 19 | ] 20 | } 21 | }, 22 | uglify: { 23 | js: { 24 | options: { 25 | sourceMap: true 26 | }, 27 | files: { 28 | 'build/js/wp-api.min.js': [ 29 | 'js/app.js', 30 | 'js/utils.js', 31 | 'js/models.js', 32 | 'js/collections.js', 33 | 'js/load.js' 34 | ] 35 | } 36 | } 37 | }, 38 | concat: { 39 | js: { 40 | src: [ 41 | 'js/app.js', 42 | 'js/utils.js', 43 | 'js/models.js', 44 | 'js/collections.js', 45 | 'js/load.js' 46 | ], 47 | dest: 'build/js/wp-api.js' 48 | } 49 | }, 50 | qunit: { 51 | all: { 52 | options: { 53 | urls: [ 'http://localhost:80/wp-content/plugins/client-js/tests/tests.html' ] 54 | } 55 | } 56 | }, 57 | watch: { 58 | files: [ 59 | 'js/*.js' 60 | ], 61 | tasks: [ 'jshint', 'jscs', 'uglify:js', 'concat:js' ] 62 | }, 63 | jscs: { 64 | src: 'js/*.js', 65 | options: { 66 | config: '.jscsrc', 67 | preset: 'wordpress' 68 | } 69 | } 70 | }); 71 | grunt.loadNpmTasks( 'grunt-contrib-jshint' ); 72 | grunt.loadNpmTasks( 'grunt-contrib-uglify' ); 73 | grunt.loadNpmTasks( 'grunt-contrib-concat' ); 74 | grunt.loadNpmTasks( 'grunt-contrib-watch' ); 75 | grunt.loadNpmTasks( 'grunt-contrib-qunit' ); 76 | grunt.loadNpmTasks( 'grunt-jscs' ); 77 | grunt.registerTask( 'build', [ 'uglify:js', 'concat:js' ] ); 78 | grunt.registerTask( 'default', [ 'jshint', 'jscs', 'build', 'qunit' ] ); 79 | grunt.registerTask( 'test', [ 'qunit:all' ] ); 80 | }; 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Backbone library for the WordPress REST API or "WP-API" 2 | ============== 3 | 4 | ## Note: 5 | 6 | This client is now bundled in WordPress core and development is happening there, see https://trac.wordpress.org. 7 | 8 | Documentation has moved to the WordPress handbook: https://developer.wordpress.org/rest-api/using-the-rest-api/backbone-javascript-client/ -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | 14 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 15 | WP_CORE_DIR=/tmp/wordpress/src/ 16 | 17 | set -ex 18 | 19 | install_wp() { 20 | mkdir -p $WP_CORE_DIR 21 | 22 | if [ $WP_VERSION == 'latest' ]; then 23 | local ARCHIVE_NAME='latest' 24 | else 25 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 26 | fi 27 | 28 | wget -nv -O /tmp/wordpress.tar.gz http://wordpress.org/${ARCHIVE_NAME}.tar.gz 29 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 30 | 31 | wget -nv -O $WP_CORE_DIR/wp-content/db.php https://raw.github.com/markoheijnen/wp-mysqli/master/db.php 32 | } 33 | 34 | install_test_suite() { 35 | # portable in-place argument for both GNU sed and Mac OSX sed 36 | if [[ $(uname -s) == 'Darwin' ]]; then 37 | local ioption='-i .bak' 38 | else 39 | local ioption='-i' 40 | fi 41 | 42 | # set up testing suite 43 | mkdir -p $WP_TESTS_DIR 44 | cd $WP_TESTS_DIR 45 | svn co --quiet http://develop.svn.wordpress.org/trunk/tests/phpunit/includes/ 46 | 47 | wget -nv -O wp-tests-config.php http://develop.svn.wordpress.org/trunk/wp-tests-config-sample.php 48 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" wp-tests-config.php 49 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" wp-tests-config.php 50 | sed $ioption "s/yourusernamehere/$DB_USER/" wp-tests-config.php 51 | sed $ioption "s/yourpasswordhere/$DB_PASS/" wp-tests-config.php 52 | sed $ioption "s|localhost|${DB_HOST}|" wp-tests-config.php 53 | } 54 | 55 | install_db() { 56 | # parse DB_HOST for port or socket references 57 | local PARTS=(${DB_HOST//\:/ }) 58 | local DB_HOSTNAME=${PARTS[0]}; 59 | local DB_SOCK_OR_PORT=${PARTS[1]}; 60 | local EXTRA="" 61 | 62 | if ! [ -z $DB_HOSTNAME ] ; then 63 | if [[ "$DB_SOCK_OR_PORT" =~ ^[0-9]+$ ]] ; then 64 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 65 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 66 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 67 | elif ! [ -z $DB_HOSTNAME ] ; then 68 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 69 | fi 70 | fi 71 | 72 | # create database 73 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 74 | } 75 | 76 | #install_test_suite 77 | install_db 78 | install_wp 79 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-api-js", 3 | "dependencies": { 4 | "jquery": "~2.1.1", 5 | "backbone": "~1.1.2", 6 | "underscore": "~1.6.0", 7 | "jquery-migrate": "~1.2.1", 8 | "qunit": "^1.22.0", 9 | "sinon-1.9.1": "http://sinonjs.org/releases/sinon-1.9.1.js", 10 | "sinon-qunit-1.0.0": "http://sinonjs.org/releases/sinon-qunit-1.0.0.js" 11 | }, 12 | "main": "build/js/wp-api.js" 13 | } 14 | -------------------------------------------------------------------------------- /build/js/wp-api.js: -------------------------------------------------------------------------------- 1 | (function( window, undefined ) { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Initialise the WP_API. 7 | */ 8 | function WP_API() { 9 | this.models = {}; 10 | this.collections = {}; 11 | this.views = {}; 12 | } 13 | 14 | window.wp = window.wp || {}; 15 | wp.api = wp.api || new WP_API(); 16 | wp.api.versionString = wp.api.versionString || 'wp/v2/'; 17 | 18 | // Alias _includes to _.contains, ensuring it is available if lodash is used. 19 | if ( ! _.isFunction( _.includes ) && _.isFunction( _.contains ) ) { 20 | _.includes = _.contains; 21 | } 22 | 23 | })( window ); 24 | 25 | (function( window, undefined ) { 26 | 27 | 'use strict'; 28 | 29 | var pad, r; 30 | 31 | window.wp = window.wp || {}; 32 | wp.api = wp.api || {}; 33 | wp.api.utils = wp.api.utils || {}; 34 | 35 | /** 36 | * ECMAScript 5 shim, adapted from MDN. 37 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString 38 | */ 39 | if ( ! Date.prototype.toISOString ) { 40 | pad = function( number ) { 41 | r = String( number ); 42 | if ( 1 === r.length ) { 43 | r = '0' + r; 44 | } 45 | 46 | return r; 47 | }; 48 | 49 | Date.prototype.toISOString = function() { 50 | return this.getUTCFullYear() + 51 | '-' + pad( this.getUTCMonth() + 1 ) + 52 | '-' + pad( this.getUTCDate() ) + 53 | 'T' + pad( this.getUTCHours() ) + 54 | ':' + pad( this.getUTCMinutes() ) + 55 | ':' + pad( this.getUTCSeconds() ) + 56 | '.' + String( ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ) ).slice( 2, 5 ) + 57 | 'Z'; 58 | }; 59 | } 60 | 61 | /** 62 | * Parse date into ISO8601 format. 63 | * 64 | * @param {Date} date. 65 | */ 66 | wp.api.utils.parseISO8601 = function( date ) { 67 | var timestamp, struct, i, k, 68 | minutesOffset = 0, 69 | numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; 70 | 71 | // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string 72 | // before falling back to any implementation-specific date parsing, so that’s what we do, even if native 73 | // implementations could be faster. 74 | // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 75 | 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 ) ) ) { 76 | 77 | // Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC. 78 | for ( i = 0; ( k = numericKeys[i] ); ++i ) { 79 | struct[k] = +struct[k] || 0; 80 | } 81 | 82 | // Allow undefined days and months. 83 | struct[2] = ( +struct[2] || 1 ) - 1; 84 | struct[3] = +struct[3] || 1; 85 | 86 | if ( 'Z' !== struct[8] && undefined !== struct[9] ) { 87 | minutesOffset = struct[10] * 60 + struct[11]; 88 | 89 | if ( '+' === struct[9] ) { 90 | minutesOffset = 0 - minutesOffset; 91 | } 92 | } 93 | 94 | timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] ); 95 | } else { 96 | timestamp = Date.parse ? Date.parse( date ) : NaN; 97 | } 98 | 99 | return timestamp; 100 | }; 101 | 102 | /** 103 | * Helper function for getting the root URL. 104 | * @return {[type]} [description] 105 | */ 106 | wp.api.utils.getRootUrl = function() { 107 | return window.location.origin ? 108 | window.location.origin + '/' : 109 | window.location.protocol + '/' + window.location.host + '/'; 110 | }; 111 | 112 | /** 113 | * Helper for capitalizing strings. 114 | */ 115 | wp.api.utils.capitalize = function( str ) { 116 | if ( _.isUndefined( str ) ) { 117 | return str; 118 | } 119 | return str.charAt( 0 ).toUpperCase() + str.slice( 1 ); 120 | }; 121 | 122 | /** 123 | * Helper function that capitilises the first word and camel cases any words starting 124 | * after dashes, removing the dashes. 125 | */ 126 | wp.api.utils.capitalizeAndCamelCaseDashes = function( str ) { 127 | if ( _.isUndefined( str ) ) { 128 | return str; 129 | } 130 | str = wp.api.utils.capitalize( str ); 131 | 132 | return wp.api.utils.camelCaseDashes( str ); 133 | }; 134 | 135 | /** 136 | * Helper function to camel case the letter after dashes, removing the dashes. 137 | */ 138 | wp.api.utils.camelCaseDashes = function( str ) { 139 | return str.replace( /-([a-z])/g, function( g ) { 140 | return g[ 1 ].toUpperCase(); 141 | } ); 142 | }; 143 | 144 | /** 145 | * Extract a route part based on negative index. 146 | * 147 | * @param {string} route The endpoint route. 148 | * @param {int} part The number of parts from the end of the route to retrieve. Default 1. 149 | * Example route `/a/b/c`: part 1 is `c`, part 2 is `b`, part 3 is `a`. 150 | * @param {string} [versionString] Version string, defaults to wp.api.versionString. 151 | * @param {boolean} [reverse] Whether to reverse the order when extracting the rout part. Optional, default true; 152 | */ 153 | wp.api.utils.extractRoutePart = function( route, part, versionString, reverse ) { 154 | var routeParts; 155 | 156 | part = part || 1; 157 | versionString = versionString || wp.api.versionString; 158 | 159 | // Remove versions string from route to avoid returning it. 160 | if ( 0 === route.indexOf( '/' + versionString ) ) { 161 | route = route.substr( versionString.length + 1 ); 162 | } 163 | 164 | routeParts = route.split( '/' ); 165 | if ( reverse ) { 166 | routeParts = routeParts.reverse(); 167 | } 168 | if ( _.isUndefined( routeParts[ --part ] ) ) { 169 | return ''; 170 | } 171 | return routeParts[ part ]; 172 | }; 173 | 174 | /** 175 | * Extract a parent name from a passed route. 176 | * 177 | * @param {string} route The route to extract a name from. 178 | */ 179 | wp.api.utils.extractParentName = function( route ) { 180 | var name, 181 | lastSlash = route.lastIndexOf( '_id>[\\d]+)/' ); 182 | 183 | if ( lastSlash < 0 ) { 184 | return ''; 185 | } 186 | name = route.substr( 0, lastSlash - 1 ); 187 | name = name.split( '/' ); 188 | name.pop(); 189 | name = name.pop(); 190 | return name; 191 | }; 192 | 193 | /** 194 | * Add args and options to a model prototype from a route's endpoints. 195 | * 196 | * @param {array} routeEndpoints Array of route endpoints. 197 | * @param {Object} modelInstance An instance of the model (or collection) 198 | * to add the args to. 199 | */ 200 | wp.api.utils.decorateFromRoute = function( routeEndpoints, modelInstance ) { 201 | 202 | /** 203 | * Build the args based on route endpoint data. 204 | */ 205 | _.each( routeEndpoints, function( routeEndpoint ) { 206 | 207 | // Add post and edit endpoints as model args. 208 | if ( _.includes( routeEndpoint.methods, 'POST' ) || _.includes( routeEndpoint.methods, 'PUT' ) ) { 209 | 210 | // Add any non empty args, merging them into the args object. 211 | if ( ! _.isEmpty( routeEndpoint.args ) ) { 212 | 213 | // Set as default if no args yet. 214 | if ( _.isEmpty( modelInstance.prototype.args ) ) { 215 | modelInstance.prototype.args = routeEndpoint.args; 216 | } else { 217 | 218 | // We already have args, merge these new args in. 219 | modelInstance.prototype.args = _.extend( modelInstance.prototype.args, routeEndpoint.args ); 220 | } 221 | } 222 | } else { 223 | 224 | // Add GET method as model options. 225 | if ( _.includes( routeEndpoint.methods, 'GET' ) ) { 226 | 227 | // Add any non empty args, merging them into the defaults object. 228 | if ( ! _.isEmpty( routeEndpoint.args ) ) { 229 | 230 | // Set as defauls if no defaults yet. 231 | if ( _.isEmpty( modelInstance.prototype.options ) ) { 232 | modelInstance.prototype.options = routeEndpoint.args; 233 | } else { 234 | 235 | // We already have options, merge these new args in. 236 | modelInstance.prototype.options = _.extend( modelInstance.prototype.options, routeEndpoint.args ); 237 | } 238 | } 239 | 240 | } 241 | } 242 | 243 | } ); 244 | 245 | }; 246 | 247 | /** 248 | * Add mixins and helpers to models depending on their defaults. 249 | * 250 | * @param {Backbone Model} model The model to attach helpers and mixins to. 251 | * @param {string} modelClassName The classname of the constructed model. 252 | * @param {Object} loadingObjects An object containing the models and collections we are building. 253 | */ 254 | wp.api.utils.addMixinsAndHelpers = function( model, modelClassName, loadingObjects ) { 255 | 256 | var hasDate = false, 257 | 258 | /** 259 | * Array of parseable dates. 260 | * 261 | * @type {string[]}. 262 | */ 263 | parseableDates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ], 264 | 265 | /** 266 | * Mixin for all content that is time stamped. 267 | * 268 | * This mixin converts between mysql timestamps and JavaScript Dates when syncing a model 269 | * to or from the server. For example, a date stored as `2015-12-27T21:22:24` on the server 270 | * gets expanded to `Sun Dec 27 2015 14:22:24 GMT-0700 (MST)` when the model is fetched. 271 | * 272 | * @type {{toJSON: toJSON, parse: parse}}. 273 | */ 274 | TimeStampedMixin = { 275 | 276 | /** 277 | * Prepare a JavaScript Date for transmitting to the server. 278 | * 279 | * This helper function accepts a field and Date object. It converts the passed Date 280 | * to an ISO string and sets that on the model field. 281 | * 282 | * @param {Date} date A JavaScript date object. WordPress expects dates in UTC. 283 | * @param {string} field The date field to set. One of 'date', 'date_gmt', 'date_modified' 284 | * or 'date_modified_gmt'. Optional, defaults to 'date'. 285 | */ 286 | setDate: function( date, field ) { 287 | var theField = field || 'date'; 288 | 289 | // Don't alter non parsable date fields. 290 | if ( _.indexOf( parseableDates, theField ) < 0 ) { 291 | return false; 292 | } 293 | 294 | this.set( theField, date.toISOString() ); 295 | }, 296 | 297 | /** 298 | * Get a JavaScript Date from the passed field. 299 | * 300 | * WordPress returns 'date' and 'date_modified' in the timezone of the server as well as 301 | * UTC dates as 'date_gmt' and 'date_modified_gmt'. Draft posts do not include UTC dates. 302 | * 303 | * @param {string} field The date field to set. One of 'date', 'date_gmt', 'date_modified' 304 | * or 'date_modified_gmt'. Optional, defaults to 'date'. 305 | */ 306 | getDate: function( field ) { 307 | var theField = field || 'date', 308 | theISODate = this.get( theField ); 309 | 310 | // Only get date fields and non null values. 311 | if ( _.indexOf( parseableDates, theField ) < 0 || _.isNull( theISODate ) ) { 312 | return false; 313 | } 314 | 315 | return new Date( wp.api.utils.parseISO8601( theISODate ) ); 316 | } 317 | }, 318 | 319 | /** 320 | * Build a helper function to retrieve related model. 321 | * 322 | * @param {string} parentModel The parent model. 323 | * @param {int} modelId The model ID if the object to request 324 | * @param {string} modelName The model name to use when constructing the model. 325 | * @param {string} embedSourcePoint Where to check the embedds object for _embed data. 326 | * @param {string} embedCheckField Which model field to check to see if the model has data. 327 | * 328 | * @return {Deferred.promise} A promise which resolves to the constructed model. 329 | */ 330 | buildModelGetter = function( parentModel, modelId, modelName, embedSourcePoint, embedCheckField ) { 331 | var getModel, embeddeds, attributes, deferred; 332 | 333 | deferred = jQuery.Deferred(); 334 | embeddeds = parentModel.get( '_embedded' ) || {}; 335 | 336 | // Verify that we have a valied object id. 337 | if ( ! _.isNumber( modelId ) || 0 === modelId ) { 338 | deferred.reject(); 339 | return deferred; 340 | } 341 | 342 | // If we have embedded object data, use that when constructing the getModel. 343 | if ( embeddeds[ embedSourcePoint ] ) { 344 | attributes = _.findWhere( embeddeds[ embedSourcePoint ], { id: modelId } ); 345 | } 346 | 347 | // Otherwise use the modelId. 348 | if ( ! attributes ) { 349 | attributes = { id: modelId }; 350 | } 351 | 352 | // Create the new getModel model. 353 | getModel = new wp.api.models[ modelName ]( attributes ); 354 | 355 | // If we didn’t have an embedded getModel, fetch the getModel data. 356 | if ( ! getModel.get( embedCheckField ) ) { 357 | getModel.fetch( { 358 | success: function( getModel ) { 359 | deferred.resolve( getModel ); 360 | }, 361 | error: function( getModel, response ) { 362 | deferred.reject( response ); 363 | } 364 | } ); 365 | } else { 366 | 367 | // Resolve with the embedded model. 368 | deferred.resolve( getModel ); 369 | } 370 | 371 | // Return a promise. 372 | return deferred.promise(); 373 | }, 374 | 375 | /** 376 | * Build a helper to retrieve a collection. 377 | * 378 | * @param {string} parentModel The parent model. 379 | * @param {string} collectionName The name to use when constructing the collection. 380 | * @param {string} embedSourcePoint Where to check the embedds object for _embed data. 381 | * @param {string} embedIndex An addiitonal optional index for the _embed data. 382 | * 383 | * @return {Deferred.promise} A promise which resolves to the constructed collection. 384 | */ 385 | buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) { 386 | /** 387 | * Returns a promise that resolves to the requested collection 388 | * 389 | * Uses the embedded data if available, otherwises fetches the 390 | * data from the server. 391 | * 392 | * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ] 393 | * collection. 394 | */ 395 | var postId, embeddeds, getObjects, 396 | classProperties = '', 397 | properties = '', 398 | deferred = jQuery.Deferred(); 399 | 400 | postId = parentModel.get( 'id' ); 401 | embeddeds = parentModel.get( '_embedded' ) || {}; 402 | 403 | // Verify that we have a valied post id. 404 | if ( ! _.isNumber( postId ) || 0 === postId ) { 405 | deferred.reject(); 406 | return deferred; 407 | } 408 | 409 | // If we have embedded getObjects data, use that when constructing the getObjects. 410 | if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) { 411 | 412 | // Some embeds also include an index offset, check for that. 413 | if ( _.isUndefined( embedIndex ) ) { 414 | 415 | // Use the embed source point directly. 416 | properties = embeddeds[ embedSourcePoint ]; 417 | } else { 418 | 419 | // Add the index to the embed source point. 420 | properties = embeddeds[ embedSourcePoint ][ embedIndex ]; 421 | } 422 | } else { 423 | 424 | // Otherwise use the postId. 425 | classProperties = { parent: postId }; 426 | } 427 | 428 | // Create the new getObjects collection. 429 | getObjects = new wp.api.collections[ collectionName ]( properties, classProperties ); 430 | 431 | // If we didn’t have embedded getObjects, fetch the getObjects data. 432 | if ( _.isUndefined( getObjects.models[0] ) ) { 433 | getObjects.fetch( { 434 | success: function( getObjects ) { 435 | 436 | // Add a helper 'parent_post' attribute onto the model. 437 | setHelperParentPost( getObjects, postId ); 438 | deferred.resolve( getObjects ); 439 | }, 440 | error: function( getModel, response ) { 441 | deferred.reject( response ); 442 | } 443 | } ); 444 | } else { 445 | 446 | // Add a helper 'parent_post' attribute onto the model. 447 | setHelperParentPost( getObjects, postId ); 448 | deferred.resolve( getObjects ); 449 | } 450 | 451 | // Return a promise. 452 | return deferred.promise(); 453 | 454 | }, 455 | 456 | /** 457 | * Set the model post parent. 458 | */ 459 | setHelperParentPost = function( collection, postId ) { 460 | 461 | // Attach post_parent id to the collection. 462 | _.each( collection.models, function( model ) { 463 | model.set( 'parent_post', postId ); 464 | } ); 465 | }, 466 | 467 | /** 468 | * Add a helper funtion to handle post Meta. 469 | */ 470 | MetaMixin = { 471 | getMeta: function() { 472 | return buildCollectionGetter( this, 'PostMeta', 'https://api.w.org/meta' ); 473 | } 474 | }, 475 | 476 | /** 477 | * Add a helper funtion to handle post Revisions. 478 | */ 479 | RevisionsMixin = { 480 | getRevisions: function() { 481 | return buildCollectionGetter( this, 'PostRevisions' ); 482 | } 483 | }, 484 | 485 | /** 486 | * Add a helper funtion to handle post Tags. 487 | */ 488 | TagsMixin = { 489 | 490 | /** 491 | * Get the tags for a post. 492 | * 493 | * @return {Deferred.promise} promise Resolves to an array of tags. 494 | */ 495 | getTags: function() { 496 | var tagIds = this.get( 'tags' ), 497 | tags = new wp.api.collections.Tags(); 498 | 499 | // Resolve with an empty array if no tags. 500 | if ( _.isEmpty( tagIds ) ) { 501 | return jQuery.Deferred().resolve( [] ); 502 | } 503 | 504 | return tags.fetch( { data: { include: tagIds } } ); 505 | }, 506 | 507 | /** 508 | * Set the tags for a post. 509 | * 510 | * Accepts an array of tag slugs, or a Tags collection. 511 | * 512 | * @param {array|Backbone.Collection} tags The tags to set on the post. 513 | * 514 | */ 515 | setTags: function( tags ) { 516 | var allTags, newTag, 517 | self = this, 518 | newTags = []; 519 | 520 | if ( _.isString( tags ) ) { 521 | return false; 522 | } 523 | 524 | // If this is an array of slugs, build a collection. 525 | if ( _.isArray( tags ) ) { 526 | 527 | // Get all the tags. 528 | allTags = new wp.api.collections.Tags(); 529 | allTags.fetch( { 530 | data: { per_page: 100 }, 531 | success: function( alltags ) { 532 | 533 | // Find the passed tags and set them up. 534 | _.each( tags, function( tag ) { 535 | newTag = new wp.api.models.Tag( alltags.findWhere( { slug: tag } ) ); 536 | 537 | // Tie the new tag to the post. 538 | newTag.set( 'parent_post', self.get( 'id' ) ); 539 | 540 | // Add the new tag to the collection. 541 | newTags.push( newTag ); 542 | } ); 543 | tags = new wp.api.collections.Tags( newTags ); 544 | self.setTagsWithCollection( tags ); 545 | } 546 | } ); 547 | 548 | } else { 549 | this.setTagsWithCollection( tags ); 550 | } 551 | }, 552 | 553 | /** 554 | * Set the tags for a post. 555 | * 556 | * Accepts a Tags collection. 557 | * 558 | * @param {array|Backbone.Collection} tags The tags to set on the post. 559 | * 560 | */ 561 | setTagsWithCollection: function( tags ) { 562 | 563 | // Pluck out the category ids. 564 | this.set( 'tags', tags.pluck( 'id' ) ); 565 | return this.save(); 566 | } 567 | }, 568 | 569 | /** 570 | * Add a helper funtion to handle post Categories. 571 | */ 572 | CategoriesMixin = { 573 | 574 | /** 575 | * Get a the categories for a post. 576 | * 577 | * @return {Deferred.promise} promise Resolves to an array of categories. 578 | */ 579 | getCategories: function() { 580 | var categoryIds = this.get( 'categories' ), 581 | categories = new wp.api.collections.Categories(); 582 | 583 | // Resolve with an empty array if no categories. 584 | if ( _.isEmpty( categoryIds ) ) { 585 | return jQuery.Deferred().resolve( [] ); 586 | } 587 | 588 | return categories.fetch( { data: { include: categoryIds } } ); 589 | }, 590 | 591 | /** 592 | * Set the categories for a post. 593 | * 594 | * Accepts an array of category slugs, or a Categories collection. 595 | * 596 | * @param {array|Backbone.Collection} categories The categories to set on the post. 597 | * 598 | */ 599 | setCategories: function( categories ) { 600 | var allCategories, newCategory, 601 | self = this, 602 | newCategories = []; 603 | 604 | if ( _.isString( categories ) ) { 605 | return false; 606 | } 607 | 608 | // If this is an array of slugs, build a collection. 609 | if ( _.isArray( categories ) ) { 610 | 611 | // Get all the categories. 612 | allCategories = new wp.api.collections.Categories(); 613 | allCategories.fetch( { 614 | data: { per_page: 100 }, 615 | success: function( allcats ) { 616 | 617 | // Find the passed categories and set them up. 618 | _.each( categories, function( category ) { 619 | newCategory = new wp.api.models.Category( allcats.findWhere( { slug: category } ) ); 620 | 621 | // Tie the new category to the post. 622 | newCategory.set( 'parent_post', self.get( 'id' ) ); 623 | 624 | // Add the new category to the collection. 625 | newCategories.push( newCategory ); 626 | } ); 627 | categories = new wp.api.collections.Categories( newCategories ); 628 | self.setCategoriesWithCollection( categories ); 629 | } 630 | } ); 631 | 632 | } else { 633 | this.setCategoriesWithCollection( categories ); 634 | } 635 | 636 | }, 637 | 638 | /** 639 | * Set the categories for a post. 640 | * 641 | * Accepts Categories collection. 642 | * 643 | * @param {array|Backbone.Collection} categories The categories to set on the post. 644 | * 645 | */ 646 | setCategoriesWithCollection: function( categories ) { 647 | 648 | // Pluck out the category ids. 649 | this.set( 'categories', categories.pluck( 'id' ) ); 650 | return this.save(); 651 | } 652 | }, 653 | 654 | /** 655 | * Add a helper function to retrieve the author user model. 656 | */ 657 | AuthorMixin = { 658 | getAuthorUser: function() { 659 | return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' ); 660 | } 661 | }, 662 | 663 | /** 664 | * Add a helper function to retrieve the featured media. 665 | */ 666 | FeaturedMediaMixin = { 667 | getFeaturedMedia: function() { 668 | return buildModelGetter( this, this.get( 'featured_media' ), 'Media', 'wp:featuredmedia', 'source_url' ); 669 | } 670 | }; 671 | 672 | // Exit if we don't have valid model defaults. 673 | if ( _.isUndefined( model.prototype.args ) ) { 674 | return model; 675 | } 676 | 677 | // Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin. 678 | _.each( parseableDates, function( theDateKey ) { 679 | if ( ! _.isUndefined( model.prototype.args[ theDateKey ] ) ) { 680 | hasDate = true; 681 | } 682 | } ); 683 | 684 | // Add the TimeStampedMixin for models that contain a date field. 685 | if ( hasDate ) { 686 | model = model.extend( TimeStampedMixin ); 687 | } 688 | 689 | // Add the AuthorMixin for models that contain an author. 690 | if ( ! _.isUndefined( model.prototype.args.author ) ) { 691 | model = model.extend( AuthorMixin ); 692 | } 693 | 694 | // Add the FeaturedMediaMixin for models that contain a featured_media. 695 | if ( ! _.isUndefined( model.prototype.args.featured_media ) ) { 696 | model = model.extend( FeaturedMediaMixin ); 697 | } 698 | 699 | // Add the CategoriesMixin for models that support categories collections. 700 | if ( ! _.isUndefined( model.prototype.args.categories ) ) { 701 | model = model.extend( CategoriesMixin ); 702 | } 703 | 704 | // Add the MetaMixin for models that support meta collections. 705 | if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Meta' ] ) ) { 706 | model = model.extend( MetaMixin ); 707 | } 708 | 709 | // Add the TagsMixin for models that support tags collections. 710 | if ( ! _.isUndefined( model.prototype.args.tags ) ) { 711 | model = model.extend( TagsMixin ); 712 | } 713 | 714 | // Add the RevisionsMixin for models that support revisions collections. 715 | if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) { 716 | model = model.extend( RevisionsMixin ); 717 | } 718 | 719 | return model; 720 | }; 721 | 722 | })( window ); 723 | 724 | /* global wpApiSettings:false */ 725 | 726 | // Suppress warning about parse function's unused "options" argument: 727 | /* jshint unused:false */ 728 | (function() { 729 | 730 | 'use strict'; 731 | 732 | var wpApiSettings = window.wpApiSettings || {}; 733 | 734 | /** 735 | * Backbone base model for all models. 736 | */ 737 | wp.api.WPApiBaseModel = Backbone.Model.extend( 738 | /** @lends WPApiBaseModel.prototype */ 739 | { 740 | /** 741 | * Set nonce header before every Backbone sync. 742 | * 743 | * @param {string} method. 744 | * @param {Backbone.Model} model. 745 | * @param {{beforeSend}, *} options. 746 | * @returns {*}. 747 | */ 748 | sync: function( method, model, options ) { 749 | var beforeSend; 750 | 751 | options = options || {}; 752 | 753 | // Remove date_gmt if null. 754 | if ( _.isNull( model.get( 'date_gmt' ) ) ) { 755 | model.unset( 'date_gmt' ); 756 | } 757 | 758 | // Remove slug if empty. 759 | if ( _.isEmpty( model.get( 'slug' ) ) ) { 760 | model.unset( 'slug' ); 761 | } 762 | 763 | if ( ! _.isUndefined( wpApiSettings.nonce ) && ! _.isNull( wpApiSettings.nonce ) ) { 764 | beforeSend = options.beforeSend; 765 | 766 | // @todo enable option for jsonp endpoints 767 | // options.dataType = 'jsonp'; 768 | 769 | options.beforeSend = function( xhr ) { 770 | xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); 771 | 772 | if ( beforeSend ) { 773 | return beforeSend.apply( this, arguments ); 774 | } 775 | }; 776 | } 777 | 778 | // Add '?force=true' to use delete method when required. 779 | if ( this.requireForceForDelete && 'delete' === method ) { 780 | model.url = model.url() + '?force=true'; 781 | } 782 | return Backbone.sync( method, model, options ); 783 | }, 784 | 785 | /** 786 | * Save is only allowed when the PUT OR POST methods are available for the endpoint. 787 | */ 788 | save: function( attrs, options ) { 789 | 790 | // Do we have the put method, then execute the save. 791 | if ( _.includes( this.methods, 'PUT' ) || _.includes( this.methods, 'POST' ) ) { 792 | 793 | // Proxy the call to the original save function. 794 | return Backbone.Model.prototype.save.call( this, attrs, options ); 795 | } else { 796 | 797 | // Otherwise bail, disallowing action. 798 | return false; 799 | } 800 | }, 801 | 802 | /** 803 | * Delete is only allowed when the DELETE method is available for the endpoint. 804 | */ 805 | destroy: function( options ) { 806 | 807 | // Do we have the DELETE method, then execute the destroy. 808 | if ( _.includes( this.methods, 'DELETE' ) ) { 809 | 810 | // Proxy the call to the original save function. 811 | return Backbone.Model.prototype.destroy.call( this, options ); 812 | } else { 813 | 814 | // Otherwise bail, disallowing action. 815 | return false; 816 | } 817 | } 818 | 819 | } 820 | ); 821 | 822 | /** 823 | * API Schema model. Contains meta information about the API. 824 | */ 825 | wp.api.models.Schema = wp.api.WPApiBaseModel.extend( 826 | /** @lends Schema.prototype */ 827 | { 828 | defaults: { 829 | _links: {}, 830 | namespace: null, 831 | routes: {} 832 | }, 833 | 834 | initialize: function( attributes, options ) { 835 | var model = this; 836 | options = options || {}; 837 | 838 | wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options ); 839 | 840 | model.apiRoot = options.apiRoot || wpApiSettings.root; 841 | model.versionString = options.versionString || wpApiSettings.versionString; 842 | }, 843 | 844 | url: function() { 845 | return this.apiRoot + this.versionString; 846 | } 847 | } 848 | ); 849 | })(); 850 | 851 | ( function() { 852 | 853 | 'use strict'; 854 | 855 | var wpApiSettings = window.wpApiSettings || {}; 856 | 857 | /** 858 | * Contains basic collection functionality such as pagination. 859 | */ 860 | wp.api.WPApiBaseCollection = Backbone.Collection.extend( 861 | /** @lends BaseCollection.prototype */ 862 | { 863 | 864 | /** 865 | * Setup default state. 866 | */ 867 | initialize: function( models, options ) { 868 | this.state = { 869 | data: {}, 870 | currentPage: null, 871 | totalPages: null, 872 | totalObjects: null 873 | }; 874 | if ( _.isUndefined( options ) ) { 875 | this.parent = ''; 876 | } else { 877 | this.parent = options.parent; 878 | } 879 | }, 880 | 881 | /** 882 | * Extend Backbone.Collection.sync to add nince and pagination support. 883 | * 884 | * Set nonce header before every Backbone sync. 885 | * 886 | * @param {string} method. 887 | * @param {Backbone.Model} model. 888 | * @param {{success}, *} options. 889 | * @returns {*}. 890 | */ 891 | sync: function( method, model, options ) { 892 | var beforeSend, success, 893 | self = this; 894 | 895 | options = options || {}; 896 | beforeSend = options.beforeSend; 897 | 898 | // If we have a localized nonce, pass that along with each sync. 899 | if ( 'undefined' !== typeof wpApiSettings.nonce ) { 900 | options.beforeSend = function( xhr ) { 901 | xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); 902 | 903 | if ( beforeSend ) { 904 | return beforeSend.apply( self, arguments ); 905 | } 906 | }; 907 | } 908 | 909 | // When reading, add pagination data. 910 | if ( 'read' === method ) { 911 | if ( options.data ) { 912 | self.state.data = _.clone( options.data ); 913 | 914 | delete self.state.data.page; 915 | } else { 916 | self.state.data = options.data = {}; 917 | } 918 | 919 | if ( 'undefined' === typeof options.data.page ) { 920 | self.state.currentPage = null; 921 | self.state.totalPages = null; 922 | self.state.totalObjects = null; 923 | } else { 924 | self.state.currentPage = options.data.page - 1; 925 | } 926 | 927 | success = options.success; 928 | options.success = function( data, textStatus, request ) { 929 | if ( ! _.isUndefined( request ) ) { 930 | self.state.totalPages = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 ); 931 | self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 ); 932 | } 933 | 934 | if ( null === self.state.currentPage ) { 935 | self.state.currentPage = 1; 936 | } else { 937 | self.state.currentPage++; 938 | } 939 | 940 | if ( success ) { 941 | return success.apply( this, arguments ); 942 | } 943 | }; 944 | } 945 | 946 | // Continue by calling Backbone's sync. 947 | return Backbone.sync( method, model, options ); 948 | }, 949 | 950 | /** 951 | * Fetches the next page of objects if a new page exists. 952 | * 953 | * @param {data: {page}} options. 954 | * @returns {*}. 955 | */ 956 | more: function( options ) { 957 | options = options || {}; 958 | options.data = options.data || {}; 959 | 960 | _.extend( options.data, this.state.data ); 961 | 962 | if ( 'undefined' === typeof options.data.page ) { 963 | if ( ! this.hasMore() ) { 964 | return false; 965 | } 966 | 967 | if ( null === this.state.currentPage || this.state.currentPage <= 1 ) { 968 | options.data.page = 2; 969 | } else { 970 | options.data.page = this.state.currentPage + 1; 971 | } 972 | } 973 | 974 | return this.fetch( options ); 975 | }, 976 | 977 | /** 978 | * Returns true if there are more pages of objects available. 979 | * 980 | * @returns null|boolean. 981 | */ 982 | hasMore: function() { 983 | if ( null === this.state.totalPages || 984 | null === this.state.totalObjects || 985 | null === this.state.currentPage ) { 986 | return null; 987 | } else { 988 | return ( this.state.currentPage < this.state.totalPages ); 989 | } 990 | } 991 | } 992 | ); 993 | 994 | } )(); 995 | 996 | ( function() { 997 | 998 | 'use strict'; 999 | 1000 | var Endpoint, initializedDeferreds = {}, 1001 | wpApiSettings = window.wpApiSettings || {}; 1002 | window.wp = window.wp || {}; 1003 | wp.api = wp.api || {}; 1004 | 1005 | // If wpApiSettings is unavailable, try the default. 1006 | if ( _.isEmpty( wpApiSettings ) ) { 1007 | wpApiSettings.root = window.location.origin + '/wp-json/'; 1008 | } 1009 | 1010 | Endpoint = Backbone.Model.extend( { 1011 | defaults: { 1012 | apiRoot: wpApiSettings.root, 1013 | versionString: wp.api.versionString, 1014 | schema: null, 1015 | models: {}, 1016 | collections: {} 1017 | }, 1018 | 1019 | /** 1020 | * Initialize the Endpoint model. 1021 | */ 1022 | initialize: function() { 1023 | var model = this, deferred; 1024 | 1025 | Backbone.Model.prototype.initialize.apply( model, arguments ); 1026 | 1027 | deferred = jQuery.Deferred(); 1028 | model.schemaConstructed = deferred.promise(); 1029 | 1030 | model.schemaModel = new wp.api.models.Schema( null, { 1031 | apiRoot: model.get( 'apiRoot' ), 1032 | versionString: model.get( 'versionString' ) 1033 | } ); 1034 | 1035 | // When the model loads, resolve the promise. 1036 | model.schemaModel.once( 'change', function() { 1037 | model.constructFromSchema(); 1038 | deferred.resolve( model ); 1039 | } ); 1040 | 1041 | if ( model.get( 'schema' ) ) { 1042 | 1043 | // Use schema supplied as model attribute. 1044 | model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) ); 1045 | } else if ( 1046 | ! _.isUndefined( sessionStorage ) && 1047 | ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) && 1048 | sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) 1049 | ) { 1050 | 1051 | // Used a cached copy of the schema model if available. 1052 | model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) ); 1053 | } else { 1054 | model.schemaModel.fetch( { 1055 | /** 1056 | * When the server returns the schema model data, store the data in a sessionCache so we don't 1057 | * have to retrieve it again for this session. Then, construct the models and collections based 1058 | * on the schema model data. 1059 | */ 1060 | success: function( newSchemaModel ) { 1061 | 1062 | // Store a copy of the schema model in the session cache if available. 1063 | if ( ! _.isUndefined( sessionStorage ) && ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) ) { 1064 | try { 1065 | sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) ); 1066 | } catch ( error ) { 1067 | 1068 | // Fail silently, fixes errors in safari private mode. 1069 | } 1070 | } 1071 | }, 1072 | 1073 | // Log the error condition. 1074 | error: function( err ) { 1075 | window.console.log( err ); 1076 | } 1077 | } ); 1078 | } 1079 | }, 1080 | 1081 | constructFromSchema: function() { 1082 | var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects, 1083 | 1084 | /** 1085 | * Set up the model and collection name mapping options. As the schema is built, the 1086 | * model and collection names will be adjusted if they are found in the mapping object. 1087 | * 1088 | * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options. 1089 | * 1090 | */ 1091 | mapping = wpApiSettings.mapping || { 1092 | models: { 1093 | 'Categories': 'Category', 1094 | 'Comments': 'Comment', 1095 | 'Pages': 'Page', 1096 | 'PagesMeta': 'PageMeta', 1097 | 'PagesRevisions': 'PageRevision', 1098 | 'Posts': 'Post', 1099 | 'PostsCategories': 'PostCategory', 1100 | 'PostsRevisions': 'PostRevision', 1101 | 'PostsTags': 'PostTag', 1102 | 'Schema': 'Schema', 1103 | 'Statuses': 'Status', 1104 | 'Tags': 'Tag', 1105 | 'Taxonomies': 'Taxonomy', 1106 | 'Types': 'Type', 1107 | 'Users': 'User' 1108 | }, 1109 | collections: { 1110 | 'PagesMeta': 'PageMeta', 1111 | 'PagesRevisions': 'PageRevisions', 1112 | 'PostsCategories': 'PostCategories', 1113 | 'PostsMeta': 'PostMeta', 1114 | 'PostsRevisions': 'PostRevisions', 1115 | 'PostsTags': 'PostTags' 1116 | } 1117 | }; 1118 | 1119 | /** 1120 | * Iterate thru the routes, picking up models and collections to build. Builds two arrays, 1121 | * one for models and one for collections. 1122 | */ 1123 | modelRoutes = []; 1124 | collectionRoutes = []; 1125 | schemaRoot = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' ); 1126 | loadingObjects = {}; 1127 | 1128 | /** 1129 | * Tracking objects for models and collections. 1130 | */ 1131 | loadingObjects.models = {}; 1132 | loadingObjects.collections = {}; 1133 | 1134 | _.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) { 1135 | 1136 | // Skip the schema root if included in the schema. 1137 | if ( index !== routeModel.get( ' versionString' ) && 1138 | index !== schemaRoot && 1139 | index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) ) 1140 | ) { 1141 | 1142 | // Single items end with a regex (or the special case 'me'). 1143 | if ( /(?:.*[+)]|\/me)$/.test( index ) ) { 1144 | modelRoutes.push( { index: index, route: route } ); 1145 | } else { 1146 | 1147 | // Collections end in a name. 1148 | collectionRoutes.push( { index: index, route: route } ); 1149 | } 1150 | } 1151 | } ); 1152 | 1153 | /** 1154 | * Construct the models. 1155 | * 1156 | * Base the class name on the route endpoint. 1157 | */ 1158 | _.each( modelRoutes, function( modelRoute ) { 1159 | 1160 | // Extract the name and any parent from the route. 1161 | var modelClassName, 1162 | routeName = wp.api.utils.extractRoutePart( modelRoute.index, 2, routeModel.get( 'versionString' ), true ), 1163 | parentName = wp.api.utils.extractRoutePart( modelRoute.index, 1, routeModel.get( 'versionString' ), false ), 1164 | routeEnd = wp.api.utils.extractRoutePart( modelRoute.index, 1, routeModel.get( 'versionString' ), true ); 1165 | 1166 | // Clear the parent part of the rouite if its actually the version string. 1167 | if ( parentName === routeModel.get( 'versionString' ) ) { 1168 | parentName = ''; 1169 | } 1170 | 1171 | // Handle the special case of the 'me' route. 1172 | if ( 'me' === routeEnd ) { 1173 | routeName = 'me'; 1174 | } 1175 | 1176 | // If the model has a parent in its route, add that to its class name. 1177 | if ( '' !== parentName && parentName !== routeName ) { 1178 | modelClassName = wp.api.utils.capitalizeAndCamelCaseDashes( parentName ) + wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 1179 | modelClassName = mapping.models[ modelClassName ] || modelClassName; 1180 | loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { 1181 | 1182 | // Return a constructed url based on the parent and id. 1183 | url: function() { 1184 | var url = 1185 | routeModel.get( 'apiRoot' ) + 1186 | routeModel.get( 'versionString' ) + 1187 | parentName + '/' + 1188 | ( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ? 1189 | ( _.isUndefined( this.get( 'parent_post' ) ) ? '' : this.get( 'parent_post' ) + '/' ) : 1190 | this.get( 'parent' ) + '/' ) + 1191 | routeName; 1192 | 1193 | if ( ! _.isUndefined( this.get( 'id' ) ) ) { 1194 | url += '/' + this.get( 'id' ); 1195 | } 1196 | return url; 1197 | }, 1198 | 1199 | // Include a reference to the original route object. 1200 | route: modelRoute, 1201 | 1202 | // Include a reference to the original class name. 1203 | name: modelClassName, 1204 | 1205 | // Include the array of route methods for easy reference. 1206 | methods: modelRoute.route.methods, 1207 | 1208 | initialize: function( attributes, options ) { 1209 | wp.api.WPApiBaseModel.prototype.initialize.call( this, attributes, options ); 1210 | 1211 | /** 1212 | * Posts and pages support trashing, other types don't support a trash 1213 | * and require that you pass ?force=true to actually delete them. 1214 | * 1215 | * @todo we should be getting trashability from the Schema, not hard coding types here. 1216 | */ 1217 | if ( 1218 | 'Posts' !== this.name && 1219 | 'Pages' !== this.name && 1220 | _.includes( this.methods, 'DELETE' ) 1221 | ) { 1222 | this.requireForceForDelete = true; 1223 | } 1224 | } 1225 | } ); 1226 | } else { 1227 | 1228 | // This is a model without a parent in its route 1229 | modelClassName = wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 1230 | modelClassName = mapping.models[ modelClassName ] || modelClassName; 1231 | loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { 1232 | 1233 | // Function that returns a constructed url based on the id. 1234 | url: function() { 1235 | var url = routeModel.get( 'apiRoot' ) + 1236 | routeModel.get( 'versionString' ) + 1237 | ( ( 'me' === routeName ) ? 'users/me' : routeName ); 1238 | 1239 | if ( ! _.isUndefined( this.get( 'id' ) ) ) { 1240 | url += '/' + this.get( 'id' ); 1241 | } 1242 | return url; 1243 | }, 1244 | 1245 | // Include a reference to the original route object. 1246 | route: modelRoute, 1247 | 1248 | // Include a reference to the original class name. 1249 | name: modelClassName, 1250 | 1251 | // Include the array of route methods for easy reference. 1252 | methods: modelRoute.route.methods 1253 | } ); 1254 | } 1255 | 1256 | // Add defaults to the new model, pulled form the endpoint. 1257 | wp.api.utils.decorateFromRoute( 1258 | modelRoute.route.endpoints, 1259 | loadingObjects.models[ modelClassName ], 1260 | routeModel.get( 'versionString' ) 1261 | ); 1262 | 1263 | } ); 1264 | 1265 | /** 1266 | * Construct the collections. 1267 | * 1268 | * Base the class name on the route endpoint. 1269 | */ 1270 | _.each( collectionRoutes, function( collectionRoute ) { 1271 | 1272 | // Extract the name and any parent from the route. 1273 | var collectionClassName, modelClassName, 1274 | routeName = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ), 1275 | parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 1, routeModel.get( 'versionString' ), false ); 1276 | 1277 | // If the collection has a parent in its route, add that to its class name. 1278 | if ( '' !== parentName && parentName !== routeName && routeModel.get( 'versionString' ) !== parentName ) { 1279 | 1280 | collectionClassName = wp.api.utils.capitalizeAndCamelCaseDashes( parentName ) + wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 1281 | modelClassName = mapping.models[ collectionClassName ] || collectionClassName; 1282 | collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; 1283 | loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { 1284 | 1285 | // Function that returns a constructed url passed on the parent. 1286 | url: function() { 1287 | return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + 1288 | parentName + '/' + this.parent + '/' + 1289 | routeName; 1290 | }, 1291 | 1292 | // Specify the model that this collection contains. 1293 | model: function( attrs, options ) { 1294 | return new loadingObjects.models[ modelClassName ]( attrs, options ); 1295 | }, 1296 | 1297 | // Include a reference to the original class name. 1298 | name: collectionClassName, 1299 | 1300 | // Include a reference to the original route object. 1301 | route: collectionRoute, 1302 | 1303 | // Include the array of route methods for easy reference. 1304 | methods: collectionRoute.route.methods 1305 | } ); 1306 | } else { 1307 | 1308 | // This is a collection without a parent in its route. 1309 | collectionClassName = wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 1310 | modelClassName = mapping.models[ collectionClassName ] || collectionClassName; 1311 | collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; 1312 | loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { 1313 | 1314 | // For the url of a root level collection, use a string. 1315 | url: function() { 1316 | return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName; 1317 | }, 1318 | 1319 | // Specify the model that this collection contains. 1320 | model: function( attrs, options ) { 1321 | return new loadingObjects.models[ modelClassName ]( attrs, options ); 1322 | }, 1323 | 1324 | // Include a reference to the original class name. 1325 | name: collectionClassName, 1326 | 1327 | // Include a reference to the original route object. 1328 | route: collectionRoute, 1329 | 1330 | // Include the array of route methods for easy reference. 1331 | methods: collectionRoute.route.methods 1332 | } ); 1333 | } 1334 | 1335 | // Add defaults to the new model, pulled form the endpoint. 1336 | wp.api.utils.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] ); 1337 | } ); 1338 | 1339 | // Add mixins and helpers for each of the models. 1340 | _.each( loadingObjects.models, function( model, index ) { 1341 | loadingObjects.models[ index ] = wp.api.utils.addMixinsAndHelpers( model, index, loadingObjects ); 1342 | } ); 1343 | 1344 | // Set the routeModel models and collections. 1345 | routeModel.set( 'models', loadingObjects.models ); 1346 | routeModel.set( 'collections', loadingObjects.collections ); 1347 | 1348 | } 1349 | 1350 | } ); 1351 | 1352 | wp.api.endpoints = new Backbone.Collection(); 1353 | 1354 | /** 1355 | * Initialize the wp-api, optionally passing the API root. 1356 | * 1357 | * @param {object} [args] 1358 | * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root. 1359 | * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root. 1360 | * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided. 1361 | */ 1362 | wp.api.init = function( args ) { 1363 | var endpoint, attributes = {}, deferred, promise; 1364 | 1365 | args = args || {}; 1366 | attributes.apiRoot = args.apiRoot || wpApiSettings.root || '/wp-json'; 1367 | attributes.versionString = args.versionString || wpApiSettings.versionString || 'wp/v2/'; 1368 | attributes.schema = args.schema || null; 1369 | if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) { 1370 | attributes.schema = wpApiSettings.schema; 1371 | } 1372 | 1373 | if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) { 1374 | 1375 | // Look for an existing copy of this endpoint 1376 | endpoint = wp.api.endpoints.findWhere( { 'apiRoot': attributes.apiRoot, 'versionString': attributes.versionString } ); 1377 | if ( ! endpoint ) { 1378 | endpoint = new Endpoint( attributes ); 1379 | } 1380 | deferred = jQuery.Deferred(); 1381 | promise = deferred.promise(); 1382 | 1383 | endpoint.schemaConstructed.done( function( resolvedEndpoint ) { 1384 | wp.api.endpoints.add( resolvedEndpoint ); 1385 | 1386 | // Map the default endpoints, extending any already present items (including Schema model). 1387 | wp.api.models = _.extend( wp.api.models, resolvedEndpoint.get( 'models' ) ); 1388 | wp.api.collections = _.extend( wp.api.collections, resolvedEndpoint.get( 'collections' ) ); 1389 | deferred.resolve( resolvedEndpoint ); 1390 | } ); 1391 | initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise; 1392 | } 1393 | return initializedDeferreds[ attributes.apiRoot + attributes.versionString ]; 1394 | }; 1395 | 1396 | /** 1397 | * Construct the default endpoints and add to an endpoints collection. 1398 | */ 1399 | 1400 | // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready. 1401 | wp.api.loadPromise = wp.api.init(); 1402 | 1403 | } )(); 1404 | -------------------------------------------------------------------------------- /build/js/wp-api.min.js: -------------------------------------------------------------------------------- 1 | !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/",!_.isFunction(_.includes)&&_.isFunction(_.contains)&&(_.includes=_.contains)}(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.capitalizeAndCamelCaseDashes=function(a){return _.isUndefined(a)?a:(a=wp.api.utils.capitalize(a),wp.api.utils.camelCaseDashes(a))},wp.api.utils.camelCaseDashes=function(a){return a.replace(/-([a-z])/g,function(a){return a[1].toUpperCase()})},wp.api.utils.extractRoutePart=function(a,b,c,d){var e;return b=b||1,c=c||wp.api.versionString,0===a.indexOf("/"+c)&&(a=a.substr(c.length+1)),e=a.split("/"),d&&(e=e.reverse()),_.isUndefined(e[--b])?"":e[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())},wp.api.utils.decorateFromRoute=function(a,b){_.each(a,function(a){_.includes(a.methods,"POST")||_.includes(a.methods,"PUT")?_.isEmpty(a.args)||(_.isEmpty(b.prototype.args)?b.prototype.args=a.args:b.prototype.args=_.extend(b.prototype.args,a.args)):_.includes(a.methods,"GET")&&(_.isEmpty(a.args)||(_.isEmpty(b.prototype.options)?b.prototype.options=a.args:b.prototype.options=_.extend(b.prototype.options,a.args)))})},wp.api.utils.addMixinsAndHelpers=function(a,b,c){var d=!1,e=["date","modified","date_gmt","modified_gmt"],f={setDate:function(a,b){var c=b||"date";return _.indexOf(e,c)<0?!1:void this.set(c,a.toISOString())},getDate:function(a){var b=a||"date",c=this.get(b);return _.indexOf(e,b)<0||_.isNull(c)?!1:new Date(wp.api.utils.parseISO8601(c))}},g=function(a,b,c,d,e){var f,g,h,i;return i=jQuery.Deferred(),g=a.get("_embedded")||{},_.isNumber(b)&&0!==b?(g[d]&&(h=_.findWhere(g[d],{id:b})),h||(h={id:b}),f=new wp.api.models[c](h),f.get(e)?i.resolve(f):f.fetch({success:function(a){i.resolve(a)},error:function(a,b){i.reject(b)}}),i.promise()):(i.reject(),i)},h=function(a,b,c,d){var e,f,g,h="",j="",k=jQuery.Deferred();return e=a.get("id"),f=a.get("_embedded")||{},_.isNumber(e)&&0!==e?(_.isUndefined(c)||_.isUndefined(f[c])?h={parent:e}:j=_.isUndefined(d)?f[c]:f[c][d],g=new wp.api.collections[b](j,h),_.isUndefined(g.models[0])?g.fetch({success:function(a){i(a,e),k.resolve(a)},error:function(a,b){k.reject(b)}}):(i(g,e),k.resolve(g)),k.promise()):(k.reject(),k)},i=function(a,b){_.each(a.models,function(a){a.set("parent_post",b)})},j={getMeta:function(){return h(this,"PostMeta","https://api.w.org/meta")}},k={getRevisions:function(){return h(this,"PostRevisions")}},l={getTags:function(){var a=this.get("tags"),b=new wp.api.collections.Tags;return _.isEmpty(a)?jQuery.Deferred().resolve([]):b.fetch({data:{include:a}})},setTags:function(a){var b,c,d=this,e=[];return _.isString(a)?!1:void(_.isArray(a)?(b=new wp.api.collections.Tags,b.fetch({data:{per_page:100},success:function(b){_.each(a,function(a){c=new wp.api.models.Tag(b.findWhere({slug:a})),c.set("parent_post",d.get("id")),e.push(c)}),a=new wp.api.collections.Tags(e),d.setTagsWithCollection(a)}})):this.setTagsWithCollection(a))},setTagsWithCollection:function(a){return this.set("tags",a.pluck("id")),this.save()}},m={getCategories:function(){var a=this.get("categories"),b=new wp.api.collections.Categories;return _.isEmpty(a)?jQuery.Deferred().resolve([]):b.fetch({data:{include:a}})},setCategories:function(a){var b,c,d=this,e=[];return _.isString(a)?!1:void(_.isArray(a)?(b=new wp.api.collections.Categories,b.fetch({data:{per_page:100},success:function(b){_.each(a,function(a){c=new wp.api.models.Category(b.findWhere({slug:a})),c.set("parent_post",d.get("id")),e.push(c)}),a=new wp.api.collections.Categories(e),d.setCategoriesWithCollection(a)}})):this.setCategoriesWithCollection(a))},setCategoriesWithCollection:function(a){return this.set("categories",a.pluck("id")),this.save()}},n={getAuthorUser:function(){return g(this,this.get("author"),"User","author","name")}},o={getFeaturedMedia:function(){return g(this,this.get("featured_media"),"Media","wp:featuredmedia","source_url")}};return _.isUndefined(a.prototype.args)?a:(_.each(e,function(b){_.isUndefined(a.prototype.args[b])||(d=!0)}),d&&(a=a.extend(f)),_.isUndefined(a.prototype.args.author)||(a=a.extend(n)),_.isUndefined(a.prototype.args.featured_media)||(a=a.extend(o)),_.isUndefined(a.prototype.args.categories)||(a=a.extend(m)),_.isUndefined(c.collections[b+"Meta"])||(a=a.extend(j)),_.isUndefined(a.prototype.args.tags)||(a=a.extend(l)),_.isUndefined(c.collections[b+"Revisions"])||(a=a.extend(k)),a)}}(window),function(){"use strict";var a=window.wpApiSettings||{};wp.api.WPApiBaseModel=Backbone.Model.extend({sync:function(b,c,d){var e;return d=d||{},_.isNull(c.get("date_gmt"))&&c.unset("date_gmt"),_.isEmpty(c.get("slug"))&&c.unset("slug"),_.isUndefined(a.nonce)||_.isNull(a.nonce)||(e=d.beforeSend,d.beforeSend=function(b){return b.setRequestHeader("X-WP-Nonce",a.nonce),e?e.apply(this,arguments):void 0}),this.requireForceForDelete&&"delete"===b&&(c.url=c.url()+"?force=true"),Backbone.sync(b,c,d)},save:function(a,b){return _.includes(this.methods,"PUT")||_.includes(this.methods,"POST")?Backbone.Model.prototype.save.call(this,a,b):!1},destroy:function(a){return _.includes(this.methods,"DELETE")?Backbone.Model.prototype.destroy.call(this,a):!1}}),wp.api.models.Schema=wp.api.WPApiBaseModel.extend({defaults:{_links:{},namespace:null,routes:{}},initialize:function(b,c){var d=this;c=c||{},wp.api.WPApiBaseModel.prototype.initialize.call(d,b,c),d.apiRoot=c.apiRoot||a.root,d.versionString=c.versionString||a.versionString},url:function(){return this.apiRoot+this.versionString}})}(),function(){"use strict";var a=window.wpApiSettings||{};wp.api.WPApiBaseCollection=Backbone.Collection.extend({initialize:function(a,b){this.state={data:{},currentPage:null,totalPages:null,totalObjects:null},_.isUndefined(b)?this.parent="":this.parent=b.parent},sync:function(b,c,d){var e,f,g=this;return d=d||{},e=d.beforeSend,"undefined"!=typeof a.nonce&&(d.beforeSend=function(b){return b.setRequestHeader("X-WP-Nonce",a.nonce),e?e.apply(g,arguments):void 0}),"read"===b&&(d.data?(g.state.data=_.clone(d.data),delete g.state.data.page):g.state.data=d.data={},"undefined"==typeof d.data.page?(g.state.currentPage=null,g.state.totalPages=null,g.state.totalObjects=null):g.state.currentPage=d.data.page-1,f=d.success,d.success=function(a,b,c){return _.isUndefined(c)||(g.state.totalPages=parseInt(c.getResponseHeader("x-wp-totalpages"),10),g.state.totalObjects=parseInt(c.getResponseHeader("x-wp-total"),10)),null===g.state.currentPage?g.state.currentPage=1:g.state.currentPage++,f?f.apply(this,arguments):void 0}),Backbone.sync(b,c,d)},more:function(a){if(a=a||{},a.data=a.data||{},_.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.currentPageregistered['wp-api'] ) ) { 18 | $scripts->registered['wp-api']->src = $src; 19 | } else { 20 | wp_register_script( 'wp-api', $src, array( 'jquery', 'underscore', 'backbone' ), '1.0', true ); 21 | } 22 | 23 | /** 24 | * @var WP_REST_Server $wp_rest_server 25 | */ 26 | global $wp_rest_server; 27 | 28 | // Ensure the rest server is intiialized. 29 | if ( empty( $wp_rest_server ) ) { 30 | /** This filter is documented in wp-includes/rest-api.php */ 31 | $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' ); 32 | $wp_rest_server = new $wp_rest_server_class(); 33 | 34 | /** This filter is documented in wp-includes/rest-api.php */ 35 | do_action( 'rest_api_init', $wp_rest_server ); 36 | } 37 | 38 | // Load the schema. 39 | $schema_request = new WP_REST_Request( 'GET', '/wp/v2' ); 40 | $schema_response = $wp_rest_server->dispatch( $schema_request ); 41 | $schema = null; 42 | if ( ! $schema_response->is_error() ) { 43 | $schema = $schema_response->get_data(); 44 | } 45 | 46 | // Localize the plugin settings and schema. 47 | $settings = array( 48 | 'root' => esc_url_raw( get_rest_url() ), 49 | 'nonce' => wp_create_nonce( 'wp_rest' ), 50 | 'versionString' => 'wp/v2/', 51 | 'schema' => $schema, 52 | 'cacheSchema' => true, 53 | ); 54 | 55 | /** 56 | * Filter the JavaScript Client settings before localizing. 57 | * 58 | * Enables modifying the config values sent to the JS client. 59 | * 60 | * @param array $settings The JS Client settings. 61 | */ 62 | $settings = apply_filters( 'rest_js_client_settings', $settings ); 63 | wp_localize_script( 'wp-api', 'wpApiSettings', $settings ); 64 | 65 | } 66 | 67 | add_action( 'wp_enqueue_scripts', 'json_api_client_js' ); 68 | add_action( 'admin_enqueue_scripts', 'json_api_client_js' ); 69 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | (function( window, undefined ) { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Initialise the WP_API. 7 | */ 8 | function WP_API() { 9 | this.models = {}; 10 | this.collections = {}; 11 | this.views = {}; 12 | } 13 | 14 | window.wp = window.wp || {}; 15 | wp.api = wp.api || new WP_API(); 16 | wp.api.versionString = wp.api.versionString || 'wp/v2/'; 17 | 18 | // Alias _includes to _.contains, ensuring it is available if lodash is used. 19 | if ( ! _.isFunction( _.includes ) && _.isFunction( _.contains ) ) { 20 | _.includes = _.contains; 21 | } 22 | 23 | })( window ); 24 | -------------------------------------------------------------------------------- /js/collections.js: -------------------------------------------------------------------------------- 1 | ( function() { 2 | 3 | 'use strict'; 4 | 5 | var wpApiSettings = window.wpApiSettings || {}; 6 | 7 | /** 8 | * Contains basic collection functionality such as pagination. 9 | */ 10 | wp.api.WPApiBaseCollection = Backbone.Collection.extend( 11 | /** @lends BaseCollection.prototype */ 12 | { 13 | 14 | /** 15 | * Setup default state. 16 | */ 17 | initialize: function( models, options ) { 18 | this.state = { 19 | data: {}, 20 | currentPage: null, 21 | totalPages: null, 22 | totalObjects: null 23 | }; 24 | if ( _.isUndefined( options ) ) { 25 | this.parent = ''; 26 | } else { 27 | this.parent = options.parent; 28 | } 29 | }, 30 | 31 | /** 32 | * Extend Backbone.Collection.sync to add nince and pagination support. 33 | * 34 | * Set nonce header before every Backbone sync. 35 | * 36 | * @param {string} method. 37 | * @param {Backbone.Model} model. 38 | * @param {{success}, *} options. 39 | * @returns {*}. 40 | */ 41 | sync: function( method, model, options ) { 42 | var beforeSend, success, 43 | self = this; 44 | 45 | options = options || {}; 46 | beforeSend = options.beforeSend; 47 | 48 | // If we have a localized nonce, pass that along with each sync. 49 | if ( 'undefined' !== typeof wpApiSettings.nonce ) { 50 | options.beforeSend = function( xhr ) { 51 | xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); 52 | 53 | if ( beforeSend ) { 54 | return beforeSend.apply( self, arguments ); 55 | } 56 | }; 57 | } 58 | 59 | // When reading, add pagination data. 60 | if ( 'read' === method ) { 61 | if ( options.data ) { 62 | self.state.data = _.clone( options.data ); 63 | 64 | delete self.state.data.page; 65 | } else { 66 | self.state.data = options.data = {}; 67 | } 68 | 69 | if ( 'undefined' === typeof options.data.page ) { 70 | self.state.currentPage = null; 71 | self.state.totalPages = null; 72 | self.state.totalObjects = null; 73 | } else { 74 | self.state.currentPage = options.data.page - 1; 75 | } 76 | 77 | success = options.success; 78 | options.success = function( data, textStatus, request ) { 79 | if ( ! _.isUndefined( request ) ) { 80 | self.state.totalPages = parseInt( request.getResponseHeader( 'x-wp-totalpages' ), 10 ); 81 | self.state.totalObjects = parseInt( request.getResponseHeader( 'x-wp-total' ), 10 ); 82 | } 83 | 84 | if ( null === self.state.currentPage ) { 85 | self.state.currentPage = 1; 86 | } else { 87 | self.state.currentPage++; 88 | } 89 | 90 | if ( success ) { 91 | return success.apply( this, arguments ); 92 | } 93 | }; 94 | } 95 | 96 | // Continue by calling Backbone's sync. 97 | return Backbone.sync( method, model, options ); 98 | }, 99 | 100 | /** 101 | * Fetches the next page of objects if a new page exists. 102 | * 103 | * @param {data: {page}} options. 104 | * @returns {*}. 105 | */ 106 | more: function( options ) { 107 | options = options || {}; 108 | options.data = options.data || {}; 109 | 110 | _.extend( options.data, this.state.data ); 111 | 112 | if ( 'undefined' === typeof options.data.page ) { 113 | if ( ! this.hasMore() ) { 114 | return false; 115 | } 116 | 117 | if ( null === this.state.currentPage || this.state.currentPage <= 1 ) { 118 | options.data.page = 2; 119 | } else { 120 | options.data.page = this.state.currentPage + 1; 121 | } 122 | } 123 | 124 | return this.fetch( options ); 125 | }, 126 | 127 | /** 128 | * Returns true if there are more pages of objects available. 129 | * 130 | * @returns null|boolean. 131 | */ 132 | hasMore: function() { 133 | if ( null === this.state.totalPages || 134 | null === this.state.totalObjects || 135 | null === this.state.currentPage ) { 136 | return null; 137 | } else { 138 | return ( this.state.currentPage < this.state.totalPages ); 139 | } 140 | } 141 | } 142 | ); 143 | 144 | } )(); 145 | -------------------------------------------------------------------------------- /js/load.js: -------------------------------------------------------------------------------- 1 | ( function() { 2 | 3 | 'use strict'; 4 | 5 | var Endpoint, initializedDeferreds = {}, 6 | wpApiSettings = window.wpApiSettings || {}; 7 | window.wp = window.wp || {}; 8 | wp.api = wp.api || {}; 9 | 10 | // If wpApiSettings is unavailable, try the default. 11 | if ( _.isEmpty( wpApiSettings ) ) { 12 | wpApiSettings.root = window.location.origin + '/wp-json/'; 13 | } 14 | 15 | Endpoint = Backbone.Model.extend( { 16 | defaults: { 17 | apiRoot: wpApiSettings.root, 18 | versionString: wp.api.versionString, 19 | schema: null, 20 | models: {}, 21 | collections: {} 22 | }, 23 | 24 | /** 25 | * Initialize the Endpoint model. 26 | */ 27 | initialize: function() { 28 | var model = this, deferred; 29 | 30 | Backbone.Model.prototype.initialize.apply( model, arguments ); 31 | 32 | deferred = jQuery.Deferred(); 33 | model.schemaConstructed = deferred.promise(); 34 | 35 | model.schemaModel = new wp.api.models.Schema( null, { 36 | apiRoot: model.get( 'apiRoot' ), 37 | versionString: model.get( 'versionString' ) 38 | } ); 39 | 40 | // When the model loads, resolve the promise. 41 | model.schemaModel.once( 'change', function() { 42 | model.constructFromSchema(); 43 | deferred.resolve( model ); 44 | } ); 45 | 46 | if ( model.get( 'schema' ) ) { 47 | 48 | // Use schema supplied as model attribute. 49 | model.schemaModel.set( model.schemaModel.parse( model.get( 'schema' ) ) ); 50 | } else if ( 51 | ! _.isUndefined( sessionStorage ) && 52 | ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) && 53 | sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) 54 | ) { 55 | 56 | // Used a cached copy of the schema model if available. 57 | model.schemaModel.set( model.schemaModel.parse( JSON.parse( sessionStorage.getItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ) ) ) ) ); 58 | } else { 59 | model.schemaModel.fetch( { 60 | /** 61 | * When the server returns the schema model data, store the data in a sessionCache so we don't 62 | * have to retrieve it again for this session. Then, construct the models and collections based 63 | * on the schema model data. 64 | */ 65 | success: function( newSchemaModel ) { 66 | 67 | // Store a copy of the schema model in the session cache if available. 68 | if ( ! _.isUndefined( sessionStorage ) && ( _.isUndefined( wpApiSettings.cacheSchema ) || wpApiSettings.cacheSchema ) ) { 69 | try { 70 | sessionStorage.setItem( 'wp-api-schema-model' + model.get( 'apiRoot' ) + model.get( 'versionString' ), JSON.stringify( newSchemaModel ) ); 71 | } catch ( error ) { 72 | 73 | // Fail silently, fixes errors in safari private mode. 74 | } 75 | } 76 | }, 77 | 78 | // Log the error condition. 79 | error: function( err ) { 80 | window.console.log( err ); 81 | } 82 | } ); 83 | } 84 | }, 85 | 86 | constructFromSchema: function() { 87 | var routeModel = this, modelRoutes, collectionRoutes, schemaRoot, loadingObjects, 88 | 89 | /** 90 | * Set up the model and collection name mapping options. As the schema is built, the 91 | * model and collection names will be adjusted if they are found in the mapping object. 92 | * 93 | * Localizing a variable wpApiSettings.mapping will over-ride the default mapping options. 94 | * 95 | */ 96 | mapping = wpApiSettings.mapping || { 97 | models: { 98 | 'Categories': 'Category', 99 | 'Comments': 'Comment', 100 | 'Pages': 'Page', 101 | 'PagesMeta': 'PageMeta', 102 | 'PagesRevisions': 'PageRevision', 103 | 'Posts': 'Post', 104 | 'PostsCategories': 'PostCategory', 105 | 'PostsRevisions': 'PostRevision', 106 | 'PostsTags': 'PostTag', 107 | 'Schema': 'Schema', 108 | 'Statuses': 'Status', 109 | 'Tags': 'Tag', 110 | 'Taxonomies': 'Taxonomy', 111 | 'Types': 'Type', 112 | 'Users': 'User' 113 | }, 114 | collections: { 115 | 'PagesMeta': 'PageMeta', 116 | 'PagesRevisions': 'PageRevisions', 117 | 'PostsCategories': 'PostCategories', 118 | 'PostsMeta': 'PostMeta', 119 | 'PostsRevisions': 'PostRevisions', 120 | 'PostsTags': 'PostTags' 121 | } 122 | }; 123 | 124 | /** 125 | * Iterate thru the routes, picking up models and collections to build. Builds two arrays, 126 | * one for models and one for collections. 127 | */ 128 | modelRoutes = []; 129 | collectionRoutes = []; 130 | schemaRoot = routeModel.get( 'apiRoot' ).replace( wp.api.utils.getRootUrl(), '' ); 131 | loadingObjects = {}; 132 | 133 | /** 134 | * Tracking objects for models and collections. 135 | */ 136 | loadingObjects.models = {}; 137 | loadingObjects.collections = {}; 138 | 139 | _.each( routeModel.schemaModel.get( 'routes' ), function( route, index ) { 140 | 141 | // Skip the schema root if included in the schema. 142 | if ( index !== routeModel.get( ' versionString' ) && 143 | index !== schemaRoot && 144 | index !== ( '/' + routeModel.get( 'versionString' ).slice( 0, -1 ) ) 145 | ) { 146 | 147 | // Single items end with a regex (or the special case 'me'). 148 | if ( /(?:.*[+)]|\/me)$/.test( index ) ) { 149 | modelRoutes.push( { index: index, route: route } ); 150 | } else { 151 | 152 | // Collections end in a name. 153 | collectionRoutes.push( { index: index, route: route } ); 154 | } 155 | } 156 | } ); 157 | 158 | /** 159 | * Construct the models. 160 | * 161 | * Base the class name on the route endpoint. 162 | */ 163 | _.each( modelRoutes, function( modelRoute ) { 164 | 165 | // Extract the name and any parent from the route. 166 | var modelClassName, 167 | routeName = wp.api.utils.extractRoutePart( modelRoute.index, 2, routeModel.get( 'versionString' ), true ), 168 | parentName = wp.api.utils.extractRoutePart( modelRoute.index, 1, routeModel.get( 'versionString' ), false ), 169 | routeEnd = wp.api.utils.extractRoutePart( modelRoute.index, 1, routeModel.get( 'versionString' ), true ); 170 | 171 | // Clear the parent part of the rouite if its actually the version string. 172 | if ( parentName === routeModel.get( 'versionString' ) ) { 173 | parentName = ''; 174 | } 175 | 176 | // Handle the special case of the 'me' route. 177 | if ( 'me' === routeEnd ) { 178 | routeName = 'me'; 179 | } 180 | 181 | // If the model has a parent in its route, add that to its class name. 182 | if ( '' !== parentName && parentName !== routeName ) { 183 | modelClassName = wp.api.utils.capitalizeAndCamelCaseDashes( parentName ) + wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 184 | modelClassName = mapping.models[ modelClassName ] || modelClassName; 185 | loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { 186 | 187 | // Return a constructed url based on the parent and id. 188 | url: function() { 189 | var url = 190 | routeModel.get( 'apiRoot' ) + 191 | routeModel.get( 'versionString' ) + 192 | parentName + '/' + 193 | ( ( _.isUndefined( this.get( 'parent' ) ) || 0 === this.get( 'parent' ) ) ? 194 | ( _.isUndefined( this.get( 'parent_post' ) ) ? '' : this.get( 'parent_post' ) + '/' ) : 195 | this.get( 'parent' ) + '/' ) + 196 | routeName; 197 | 198 | if ( ! _.isUndefined( this.get( 'id' ) ) ) { 199 | url += '/' + this.get( 'id' ); 200 | } 201 | return url; 202 | }, 203 | 204 | // Include a reference to the original route object. 205 | route: modelRoute, 206 | 207 | // Include a reference to the original class name. 208 | name: modelClassName, 209 | 210 | // Include the array of route methods for easy reference. 211 | methods: modelRoute.route.methods, 212 | 213 | initialize: function( attributes, options ) { 214 | wp.api.WPApiBaseModel.prototype.initialize.call( this, attributes, options ); 215 | 216 | /** 217 | * Posts and pages support trashing, other types don't support a trash 218 | * and require that you pass ?force=true to actually delete them. 219 | * 220 | * @todo we should be getting trashability from the Schema, not hard coding types here. 221 | */ 222 | if ( 223 | 'Posts' !== this.name && 224 | 'Pages' !== this.name && 225 | _.includes( this.methods, 'DELETE' ) 226 | ) { 227 | this.requireForceForDelete = true; 228 | } 229 | } 230 | } ); 231 | } else { 232 | 233 | // This is a model without a parent in its route 234 | modelClassName = wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 235 | modelClassName = mapping.models[ modelClassName ] || modelClassName; 236 | loadingObjects.models[ modelClassName ] = wp.api.WPApiBaseModel.extend( { 237 | 238 | // Function that returns a constructed url based on the id. 239 | url: function() { 240 | var url = routeModel.get( 'apiRoot' ) + 241 | routeModel.get( 'versionString' ) + 242 | ( ( 'me' === routeName ) ? 'users/me' : routeName ); 243 | 244 | if ( ! _.isUndefined( this.get( 'id' ) ) ) { 245 | url += '/' + this.get( 'id' ); 246 | } 247 | return url; 248 | }, 249 | 250 | // Include a reference to the original route object. 251 | route: modelRoute, 252 | 253 | // Include a reference to the original class name. 254 | name: modelClassName, 255 | 256 | // Include the array of route methods for easy reference. 257 | methods: modelRoute.route.methods 258 | } ); 259 | } 260 | 261 | // Add defaults to the new model, pulled form the endpoint. 262 | wp.api.utils.decorateFromRoute( 263 | modelRoute.route.endpoints, 264 | loadingObjects.models[ modelClassName ], 265 | routeModel.get( 'versionString' ) 266 | ); 267 | 268 | } ); 269 | 270 | /** 271 | * Construct the collections. 272 | * 273 | * Base the class name on the route endpoint. 274 | */ 275 | _.each( collectionRoutes, function( collectionRoute ) { 276 | 277 | // Extract the name and any parent from the route. 278 | var collectionClassName, modelClassName, 279 | routeName = collectionRoute.index.slice( collectionRoute.index.lastIndexOf( '/' ) + 1 ), 280 | parentName = wp.api.utils.extractRoutePart( collectionRoute.index, 1, routeModel.get( 'versionString' ), false ); 281 | 282 | // If the collection has a parent in its route, add that to its class name. 283 | if ( '' !== parentName && parentName !== routeName && routeModel.get( 'versionString' ) !== parentName ) { 284 | 285 | collectionClassName = wp.api.utils.capitalizeAndCamelCaseDashes( parentName ) + wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 286 | modelClassName = mapping.models[ collectionClassName ] || collectionClassName; 287 | collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; 288 | loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { 289 | 290 | // Function that returns a constructed url passed on the parent. 291 | url: function() { 292 | return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + 293 | parentName + '/' + this.parent + '/' + 294 | routeName; 295 | }, 296 | 297 | // Specify the model that this collection contains. 298 | model: function( attrs, options ) { 299 | return new loadingObjects.models[ modelClassName ]( attrs, options ); 300 | }, 301 | 302 | // Include a reference to the original class name. 303 | name: collectionClassName, 304 | 305 | // Include a reference to the original route object. 306 | route: collectionRoute, 307 | 308 | // Include the array of route methods for easy reference. 309 | methods: collectionRoute.route.methods 310 | } ); 311 | } else { 312 | 313 | // This is a collection without a parent in its route. 314 | collectionClassName = wp.api.utils.capitalizeAndCamelCaseDashes( routeName ); 315 | modelClassName = mapping.models[ collectionClassName ] || collectionClassName; 316 | collectionClassName = mapping.collections[ collectionClassName ] || collectionClassName; 317 | loadingObjects.collections[ collectionClassName ] = wp.api.WPApiBaseCollection.extend( { 318 | 319 | // For the url of a root level collection, use a string. 320 | url: function() { 321 | return routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + routeName; 322 | }, 323 | 324 | // Specify the model that this collection contains. 325 | model: function( attrs, options ) { 326 | return new loadingObjects.models[ modelClassName ]( attrs, options ); 327 | }, 328 | 329 | // Include a reference to the original class name. 330 | name: collectionClassName, 331 | 332 | // Include a reference to the original route object. 333 | route: collectionRoute, 334 | 335 | // Include the array of route methods for easy reference. 336 | methods: collectionRoute.route.methods 337 | } ); 338 | } 339 | 340 | // Add defaults to the new model, pulled form the endpoint. 341 | wp.api.utils.decorateFromRoute( collectionRoute.route.endpoints, loadingObjects.collections[ collectionClassName ] ); 342 | } ); 343 | 344 | // Add mixins and helpers for each of the models. 345 | _.each( loadingObjects.models, function( model, index ) { 346 | loadingObjects.models[ index ] = wp.api.utils.addMixinsAndHelpers( model, index, loadingObjects ); 347 | } ); 348 | 349 | // Set the routeModel models and collections. 350 | routeModel.set( 'models', loadingObjects.models ); 351 | routeModel.set( 'collections', loadingObjects.collections ); 352 | 353 | } 354 | 355 | } ); 356 | 357 | wp.api.endpoints = new Backbone.Collection(); 358 | 359 | /** 360 | * Initialize the wp-api, optionally passing the API root. 361 | * 362 | * @param {object} [args] 363 | * @param {string} [args.apiRoot] The api root. Optional, defaults to wpApiSettings.root. 364 | * @param {string} [args.versionString] The version string. Optional, defaults to wpApiSettings.root. 365 | * @param {object} [args.schema] The schema. Optional, will be fetched from API if not provided. 366 | */ 367 | wp.api.init = function( args ) { 368 | var endpoint, attributes = {}, deferred, promise; 369 | 370 | args = args || {}; 371 | attributes.apiRoot = args.apiRoot || wpApiSettings.root || '/wp-json'; 372 | attributes.versionString = args.versionString || wpApiSettings.versionString || 'wp/v2/'; 373 | attributes.schema = args.schema || null; 374 | if ( ! attributes.schema && attributes.apiRoot === wpApiSettings.root && attributes.versionString === wpApiSettings.versionString ) { 375 | attributes.schema = wpApiSettings.schema; 376 | } 377 | 378 | if ( ! initializedDeferreds[ attributes.apiRoot + attributes.versionString ] ) { 379 | 380 | // Look for an existing copy of this endpoint 381 | endpoint = wp.api.endpoints.findWhere( { 'apiRoot': attributes.apiRoot, 'versionString': attributes.versionString } ); 382 | if ( ! endpoint ) { 383 | endpoint = new Endpoint( attributes ); 384 | } 385 | deferred = jQuery.Deferred(); 386 | promise = deferred.promise(); 387 | 388 | endpoint.schemaConstructed.done( function( resolvedEndpoint ) { 389 | wp.api.endpoints.add( resolvedEndpoint ); 390 | 391 | // Map the default endpoints, extending any already present items (including Schema model). 392 | wp.api.models = _.extend( wp.api.models, resolvedEndpoint.get( 'models' ) ); 393 | wp.api.collections = _.extend( wp.api.collections, resolvedEndpoint.get( 'collections' ) ); 394 | deferred.resolve( resolvedEndpoint ); 395 | } ); 396 | initializedDeferreds[ attributes.apiRoot + attributes.versionString ] = promise; 397 | } 398 | return initializedDeferreds[ attributes.apiRoot + attributes.versionString ]; 399 | }; 400 | 401 | /** 402 | * Construct the default endpoints and add to an endpoints collection. 403 | */ 404 | 405 | // The wp.api.init function returns a promise that will resolve with the endpoint once it is ready. 406 | wp.api.loadPromise = wp.api.init(); 407 | 408 | } )(); 409 | -------------------------------------------------------------------------------- /js/models.js: -------------------------------------------------------------------------------- 1 | /* global wpApiSettings:false */ 2 | 3 | // Suppress warning about parse function's unused "options" argument: 4 | /* jshint unused:false */ 5 | (function() { 6 | 7 | 'use strict'; 8 | 9 | var wpApiSettings = window.wpApiSettings || {}; 10 | 11 | /** 12 | * Backbone base model for all models. 13 | */ 14 | wp.api.WPApiBaseModel = Backbone.Model.extend( 15 | /** @lends WPApiBaseModel.prototype */ 16 | { 17 | /** 18 | * Set nonce header before every Backbone sync. 19 | * 20 | * @param {string} method. 21 | * @param {Backbone.Model} model. 22 | * @param {{beforeSend}, *} options. 23 | * @returns {*}. 24 | */ 25 | sync: function( method, model, options ) { 26 | var beforeSend; 27 | 28 | options = options || {}; 29 | 30 | // Remove date_gmt if null. 31 | if ( _.isNull( model.get( 'date_gmt' ) ) ) { 32 | model.unset( 'date_gmt' ); 33 | } 34 | 35 | // Remove slug if empty. 36 | if ( _.isEmpty( model.get( 'slug' ) ) ) { 37 | model.unset( 'slug' ); 38 | } 39 | 40 | if ( ! _.isUndefined( wpApiSettings.nonce ) && ! _.isNull( wpApiSettings.nonce ) ) { 41 | beforeSend = options.beforeSend; 42 | 43 | // @todo enable option for jsonp endpoints 44 | // options.dataType = 'jsonp'; 45 | 46 | options.beforeSend = function( xhr ) { 47 | xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce ); 48 | 49 | if ( beforeSend ) { 50 | return beforeSend.apply( this, arguments ); 51 | } 52 | }; 53 | } 54 | 55 | // Add '?force=true' to use delete method when required. 56 | if ( this.requireForceForDelete && 'delete' === method ) { 57 | model.url = model.url() + '?force=true'; 58 | } 59 | return Backbone.sync( method, model, options ); 60 | }, 61 | 62 | /** 63 | * Save is only allowed when the PUT OR POST methods are available for the endpoint. 64 | */ 65 | save: function( attrs, options ) { 66 | 67 | // Do we have the put method, then execute the save. 68 | if ( _.includes( this.methods, 'PUT' ) || _.includes( this.methods, 'POST' ) ) { 69 | 70 | // Proxy the call to the original save function. 71 | return Backbone.Model.prototype.save.call( this, attrs, options ); 72 | } else { 73 | 74 | // Otherwise bail, disallowing action. 75 | return false; 76 | } 77 | }, 78 | 79 | /** 80 | * Delete is only allowed when the DELETE method is available for the endpoint. 81 | */ 82 | destroy: function( options ) { 83 | 84 | // Do we have the DELETE method, then execute the destroy. 85 | if ( _.includes( this.methods, 'DELETE' ) ) { 86 | 87 | // Proxy the call to the original save function. 88 | return Backbone.Model.prototype.destroy.call( this, options ); 89 | } else { 90 | 91 | // Otherwise bail, disallowing action. 92 | return false; 93 | } 94 | } 95 | 96 | } 97 | ); 98 | 99 | /** 100 | * API Schema model. Contains meta information about the API. 101 | */ 102 | wp.api.models.Schema = wp.api.WPApiBaseModel.extend( 103 | /** @lends Schema.prototype */ 104 | { 105 | defaults: { 106 | _links: {}, 107 | namespace: null, 108 | routes: {} 109 | }, 110 | 111 | initialize: function( attributes, options ) { 112 | var model = this; 113 | options = options || {}; 114 | 115 | wp.api.WPApiBaseModel.prototype.initialize.call( model, attributes, options ); 116 | 117 | model.apiRoot = options.apiRoot || wpApiSettings.root; 118 | model.versionString = options.versionString || wpApiSettings.versionString; 119 | }, 120 | 121 | url: function() { 122 | return this.apiRoot + this.versionString; 123 | } 124 | } 125 | ); 126 | })(); 127 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | (function( window, undefined ) { 2 | 3 | 'use strict'; 4 | 5 | var pad, r; 6 | 7 | window.wp = window.wp || {}; 8 | wp.api = wp.api || {}; 9 | wp.api.utils = wp.api.utils || {}; 10 | 11 | /** 12 | * ECMAScript 5 shim, adapted from MDN. 13 | * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString 14 | */ 15 | if ( ! Date.prototype.toISOString ) { 16 | pad = function( number ) { 17 | r = String( number ); 18 | if ( 1 === r.length ) { 19 | r = '0' + r; 20 | } 21 | 22 | return r; 23 | }; 24 | 25 | Date.prototype.toISOString = function() { 26 | return this.getUTCFullYear() + 27 | '-' + pad( this.getUTCMonth() + 1 ) + 28 | '-' + pad( this.getUTCDate() ) + 29 | 'T' + pad( this.getUTCHours() ) + 30 | ':' + pad( this.getUTCMinutes() ) + 31 | ':' + pad( this.getUTCSeconds() ) + 32 | '.' + String( ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ) ).slice( 2, 5 ) + 33 | 'Z'; 34 | }; 35 | } 36 | 37 | /** 38 | * Parse date into ISO8601 format. 39 | * 40 | * @param {Date} date. 41 | */ 42 | wp.api.utils.parseISO8601 = function( date ) { 43 | var timestamp, struct, i, k, 44 | minutesOffset = 0, 45 | numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; 46 | 47 | // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string 48 | // before falling back to any implementation-specific date parsing, so that’s what we do, even if native 49 | // implementations could be faster. 50 | // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 51 | 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 ) ) ) { 52 | 53 | // Avoid NaN timestamps caused by “undefined” values being passed to Date.UTC. 54 | for ( i = 0; ( k = numericKeys[i] ); ++i ) { 55 | struct[k] = +struct[k] || 0; 56 | } 57 | 58 | // Allow undefined days and months. 59 | struct[2] = ( +struct[2] || 1 ) - 1; 60 | struct[3] = +struct[3] || 1; 61 | 62 | if ( 'Z' !== struct[8] && undefined !== struct[9] ) { 63 | minutesOffset = struct[10] * 60 + struct[11]; 64 | 65 | if ( '+' === struct[9] ) { 66 | minutesOffset = 0 - minutesOffset; 67 | } 68 | } 69 | 70 | timestamp = Date.UTC( struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7] ); 71 | } else { 72 | timestamp = Date.parse ? Date.parse( date ) : NaN; 73 | } 74 | 75 | return timestamp; 76 | }; 77 | 78 | /** 79 | * Helper function for getting the root URL. 80 | * @return {[type]} [description] 81 | */ 82 | wp.api.utils.getRootUrl = function() { 83 | return window.location.origin ? 84 | window.location.origin + '/' : 85 | window.location.protocol + '/' + window.location.host + '/'; 86 | }; 87 | 88 | /** 89 | * Helper for capitalizing strings. 90 | */ 91 | wp.api.utils.capitalize = function( str ) { 92 | if ( _.isUndefined( str ) ) { 93 | return str; 94 | } 95 | return str.charAt( 0 ).toUpperCase() + str.slice( 1 ); 96 | }; 97 | 98 | /** 99 | * Helper function that capitilises the first word and camel cases any words starting 100 | * after dashes, removing the dashes. 101 | */ 102 | wp.api.utils.capitalizeAndCamelCaseDashes = function( str ) { 103 | if ( _.isUndefined( str ) ) { 104 | return str; 105 | } 106 | str = wp.api.utils.capitalize( str ); 107 | 108 | return wp.api.utils.camelCaseDashes( str ); 109 | }; 110 | 111 | /** 112 | * Helper function to camel case the letter after dashes, removing the dashes. 113 | */ 114 | wp.api.utils.camelCaseDashes = function( str ) { 115 | return str.replace( /-([a-z])/g, function( g ) { 116 | return g[ 1 ].toUpperCase(); 117 | } ); 118 | }; 119 | 120 | /** 121 | * Extract a route part based on negative index. 122 | * 123 | * @param {string} route The endpoint route. 124 | * @param {int} part The number of parts from the end of the route to retrieve. Default 1. 125 | * Example route `/a/b/c`: part 1 is `c`, part 2 is `b`, part 3 is `a`. 126 | * @param {string} [versionString] Version string, defaults to wp.api.versionString. 127 | * @param {boolean} [reverse] Whether to reverse the order when extracting the rout part. Optional, default true; 128 | */ 129 | wp.api.utils.extractRoutePart = function( route, part, versionString, reverse ) { 130 | var routeParts; 131 | 132 | part = part || 1; 133 | versionString = versionString || wp.api.versionString; 134 | 135 | // Remove versions string from route to avoid returning it. 136 | if ( 0 === route.indexOf( '/' + versionString ) ) { 137 | route = route.substr( versionString.length + 1 ); 138 | } 139 | 140 | routeParts = route.split( '/' ); 141 | if ( reverse ) { 142 | routeParts = routeParts.reverse(); 143 | } 144 | if ( _.isUndefined( routeParts[ --part ] ) ) { 145 | return ''; 146 | } 147 | return routeParts[ part ]; 148 | }; 149 | 150 | /** 151 | * Extract a parent name from a passed route. 152 | * 153 | * @param {string} route The route to extract a name from. 154 | */ 155 | wp.api.utils.extractParentName = function( route ) { 156 | var name, 157 | lastSlash = route.lastIndexOf( '_id>[\\d]+)/' ); 158 | 159 | if ( lastSlash < 0 ) { 160 | return ''; 161 | } 162 | name = route.substr( 0, lastSlash - 1 ); 163 | name = name.split( '/' ); 164 | name.pop(); 165 | name = name.pop(); 166 | return name; 167 | }; 168 | 169 | /** 170 | * Add args and options to a model prototype from a route's endpoints. 171 | * 172 | * @param {array} routeEndpoints Array of route endpoints. 173 | * @param {Object} modelInstance An instance of the model (or collection) 174 | * to add the args to. 175 | */ 176 | wp.api.utils.decorateFromRoute = function( routeEndpoints, modelInstance ) { 177 | 178 | /** 179 | * Build the args based on route endpoint data. 180 | */ 181 | _.each( routeEndpoints, function( routeEndpoint ) { 182 | 183 | // Add post and edit endpoints as model args. 184 | if ( _.includes( routeEndpoint.methods, 'POST' ) || _.includes( routeEndpoint.methods, 'PUT' ) ) { 185 | 186 | // Add any non empty args, merging them into the args object. 187 | if ( ! _.isEmpty( routeEndpoint.args ) ) { 188 | 189 | // Set as default if no args yet. 190 | if ( _.isEmpty( modelInstance.prototype.args ) ) { 191 | modelInstance.prototype.args = routeEndpoint.args; 192 | } else { 193 | 194 | // We already have args, merge these new args in. 195 | modelInstance.prototype.args = _.extend( modelInstance.prototype.args, routeEndpoint.args ); 196 | } 197 | } 198 | } else { 199 | 200 | // Add GET method as model options. 201 | if ( _.includes( routeEndpoint.methods, 'GET' ) ) { 202 | 203 | // Add any non empty args, merging them into the defaults object. 204 | if ( ! _.isEmpty( routeEndpoint.args ) ) { 205 | 206 | // Set as defauls if no defaults yet. 207 | if ( _.isEmpty( modelInstance.prototype.options ) ) { 208 | modelInstance.prototype.options = routeEndpoint.args; 209 | } else { 210 | 211 | // We already have options, merge these new args in. 212 | modelInstance.prototype.options = _.extend( modelInstance.prototype.options, routeEndpoint.args ); 213 | } 214 | } 215 | 216 | } 217 | } 218 | 219 | } ); 220 | 221 | }; 222 | 223 | /** 224 | * Add mixins and helpers to models depending on their defaults. 225 | * 226 | * @param {Backbone Model} model The model to attach helpers and mixins to. 227 | * @param {string} modelClassName The classname of the constructed model. 228 | * @param {Object} loadingObjects An object containing the models and collections we are building. 229 | */ 230 | wp.api.utils.addMixinsAndHelpers = function( model, modelClassName, loadingObjects ) { 231 | 232 | var hasDate = false, 233 | 234 | /** 235 | * Array of parseable dates. 236 | * 237 | * @type {string[]}. 238 | */ 239 | parseableDates = [ 'date', 'modified', 'date_gmt', 'modified_gmt' ], 240 | 241 | /** 242 | * Mixin for all content that is time stamped. 243 | * 244 | * This mixin converts between mysql timestamps and JavaScript Dates when syncing a model 245 | * to or from the server. For example, a date stored as `2015-12-27T21:22:24` on the server 246 | * gets expanded to `Sun Dec 27 2015 14:22:24 GMT-0700 (MST)` when the model is fetched. 247 | * 248 | * @type {{toJSON: toJSON, parse: parse}}. 249 | */ 250 | TimeStampedMixin = { 251 | 252 | /** 253 | * Prepare a JavaScript Date for transmitting to the server. 254 | * 255 | * This helper function accepts a field and Date object. It converts the passed Date 256 | * to an ISO string and sets that on the model field. 257 | * 258 | * @param {Date} date A JavaScript date object. WordPress expects dates in UTC. 259 | * @param {string} field The date field to set. One of 'date', 'date_gmt', 'date_modified' 260 | * or 'date_modified_gmt'. Optional, defaults to 'date'. 261 | */ 262 | setDate: function( date, field ) { 263 | var theField = field || 'date'; 264 | 265 | // Don't alter non parsable date fields. 266 | if ( _.indexOf( parseableDates, theField ) < 0 ) { 267 | return false; 268 | } 269 | 270 | this.set( theField, date.toISOString() ); 271 | }, 272 | 273 | /** 274 | * Get a JavaScript Date from the passed field. 275 | * 276 | * WordPress returns 'date' and 'date_modified' in the timezone of the server as well as 277 | * UTC dates as 'date_gmt' and 'date_modified_gmt'. Draft posts do not include UTC dates. 278 | * 279 | * @param {string} field The date field to set. One of 'date', 'date_gmt', 'date_modified' 280 | * or 'date_modified_gmt'. Optional, defaults to 'date'. 281 | */ 282 | getDate: function( field ) { 283 | var theField = field || 'date', 284 | theISODate = this.get( theField ); 285 | 286 | // Only get date fields and non null values. 287 | if ( _.indexOf( parseableDates, theField ) < 0 || _.isNull( theISODate ) ) { 288 | return false; 289 | } 290 | 291 | return new Date( wp.api.utils.parseISO8601( theISODate ) ); 292 | } 293 | }, 294 | 295 | /** 296 | * Build a helper function to retrieve related model. 297 | * 298 | * @param {string} parentModel The parent model. 299 | * @param {int} modelId The model ID if the object to request 300 | * @param {string} modelName The model name to use when constructing the model. 301 | * @param {string} embedSourcePoint Where to check the embedds object for _embed data. 302 | * @param {string} embedCheckField Which model field to check to see if the model has data. 303 | * 304 | * @return {Deferred.promise} A promise which resolves to the constructed model. 305 | */ 306 | buildModelGetter = function( parentModel, modelId, modelName, embedSourcePoint, embedCheckField ) { 307 | var getModel, embeddeds, attributes, deferred; 308 | 309 | deferred = jQuery.Deferred(); 310 | embeddeds = parentModel.get( '_embedded' ) || {}; 311 | 312 | // Verify that we have a valied object id. 313 | if ( ! _.isNumber( modelId ) || 0 === modelId ) { 314 | deferred.reject(); 315 | return deferred; 316 | } 317 | 318 | // If we have embedded object data, use that when constructing the getModel. 319 | if ( embeddeds[ embedSourcePoint ] ) { 320 | attributes = _.findWhere( embeddeds[ embedSourcePoint ], { id: modelId } ); 321 | } 322 | 323 | // Otherwise use the modelId. 324 | if ( ! attributes ) { 325 | attributes = { id: modelId }; 326 | } 327 | 328 | // Create the new getModel model. 329 | getModel = new wp.api.models[ modelName ]( attributes ); 330 | 331 | // If we didn’t have an embedded getModel, fetch the getModel data. 332 | if ( ! getModel.get( embedCheckField ) ) { 333 | getModel.fetch( { 334 | success: function( getModel ) { 335 | deferred.resolve( getModel ); 336 | }, 337 | error: function( getModel, response ) { 338 | deferred.reject( response ); 339 | } 340 | } ); 341 | } else { 342 | 343 | // Resolve with the embedded model. 344 | deferred.resolve( getModel ); 345 | } 346 | 347 | // Return a promise. 348 | return deferred.promise(); 349 | }, 350 | 351 | /** 352 | * Build a helper to retrieve a collection. 353 | * 354 | * @param {string} parentModel The parent model. 355 | * @param {string} collectionName The name to use when constructing the collection. 356 | * @param {string} embedSourcePoint Where to check the embedds object for _embed data. 357 | * @param {string} embedIndex An addiitonal optional index for the _embed data. 358 | * 359 | * @return {Deferred.promise} A promise which resolves to the constructed collection. 360 | */ 361 | buildCollectionGetter = function( parentModel, collectionName, embedSourcePoint, embedIndex ) { 362 | /** 363 | * Returns a promise that resolves to the requested collection 364 | * 365 | * Uses the embedded data if available, otherwises fetches the 366 | * data from the server. 367 | * 368 | * @return {Deferred.promise} promise Resolves to a wp.api.collections[ collectionName ] 369 | * collection. 370 | */ 371 | var postId, embeddeds, getObjects, 372 | classProperties = '', 373 | properties = '', 374 | deferred = jQuery.Deferred(); 375 | 376 | postId = parentModel.get( 'id' ); 377 | embeddeds = parentModel.get( '_embedded' ) || {}; 378 | 379 | // Verify that we have a valied post id. 380 | if ( ! _.isNumber( postId ) || 0 === postId ) { 381 | deferred.reject(); 382 | return deferred; 383 | } 384 | 385 | // If we have embedded getObjects data, use that when constructing the getObjects. 386 | if ( ! _.isUndefined( embedSourcePoint ) && ! _.isUndefined( embeddeds[ embedSourcePoint ] ) ) { 387 | 388 | // Some embeds also include an index offset, check for that. 389 | if ( _.isUndefined( embedIndex ) ) { 390 | 391 | // Use the embed source point directly. 392 | properties = embeddeds[ embedSourcePoint ]; 393 | } else { 394 | 395 | // Add the index to the embed source point. 396 | properties = embeddeds[ embedSourcePoint ][ embedIndex ]; 397 | } 398 | } else { 399 | 400 | // Otherwise use the postId. 401 | classProperties = { parent: postId }; 402 | } 403 | 404 | // Create the new getObjects collection. 405 | getObjects = new wp.api.collections[ collectionName ]( properties, classProperties ); 406 | 407 | // If we didn’t have embedded getObjects, fetch the getObjects data. 408 | if ( _.isUndefined( getObjects.models[0] ) ) { 409 | getObjects.fetch( { 410 | success: function( getObjects ) { 411 | 412 | // Add a helper 'parent_post' attribute onto the model. 413 | setHelperParentPost( getObjects, postId ); 414 | deferred.resolve( getObjects ); 415 | }, 416 | error: function( getModel, response ) { 417 | deferred.reject( response ); 418 | } 419 | } ); 420 | } else { 421 | 422 | // Add a helper 'parent_post' attribute onto the model. 423 | setHelperParentPost( getObjects, postId ); 424 | deferred.resolve( getObjects ); 425 | } 426 | 427 | // Return a promise. 428 | return deferred.promise(); 429 | 430 | }, 431 | 432 | /** 433 | * Set the model post parent. 434 | */ 435 | setHelperParentPost = function( collection, postId ) { 436 | 437 | // Attach post_parent id to the collection. 438 | _.each( collection.models, function( model ) { 439 | model.set( 'parent_post', postId ); 440 | } ); 441 | }, 442 | 443 | /** 444 | * Add a helper funtion to handle post Meta. 445 | */ 446 | MetaMixin = { 447 | getMeta: function() { 448 | return buildCollectionGetter( this, 'PostMeta', 'https://api.w.org/meta' ); 449 | } 450 | }, 451 | 452 | /** 453 | * Add a helper funtion to handle post Revisions. 454 | */ 455 | RevisionsMixin = { 456 | getRevisions: function() { 457 | return buildCollectionGetter( this, 'PostRevisions' ); 458 | } 459 | }, 460 | 461 | /** 462 | * Add a helper funtion to handle post Tags. 463 | */ 464 | TagsMixin = { 465 | 466 | /** 467 | * Get the tags for a post. 468 | * 469 | * @return {Deferred.promise} promise Resolves to an array of tags. 470 | */ 471 | getTags: function() { 472 | var tagIds = this.get( 'tags' ), 473 | tags = new wp.api.collections.Tags(); 474 | 475 | // Resolve with an empty array if no tags. 476 | if ( _.isEmpty( tagIds ) ) { 477 | return jQuery.Deferred().resolve( [] ); 478 | } 479 | 480 | return tags.fetch( { data: { include: tagIds } } ); 481 | }, 482 | 483 | /** 484 | * Set the tags for a post. 485 | * 486 | * Accepts an array of tag slugs, or a Tags collection. 487 | * 488 | * @param {array|Backbone.Collection} tags The tags to set on the post. 489 | * 490 | */ 491 | setTags: function( tags ) { 492 | var allTags, newTag, 493 | self = this, 494 | newTags = []; 495 | 496 | if ( _.isString( tags ) ) { 497 | return false; 498 | } 499 | 500 | // If this is an array of slugs, build a collection. 501 | if ( _.isArray( tags ) ) { 502 | 503 | // Get all the tags. 504 | allTags = new wp.api.collections.Tags(); 505 | allTags.fetch( { 506 | data: { per_page: 100 }, 507 | success: function( alltags ) { 508 | 509 | // Find the passed tags and set them up. 510 | _.each( tags, function( tag ) { 511 | newTag = new wp.api.models.Tag( alltags.findWhere( { slug: tag } ) ); 512 | 513 | // Tie the new tag to the post. 514 | newTag.set( 'parent_post', self.get( 'id' ) ); 515 | 516 | // Add the new tag to the collection. 517 | newTags.push( newTag ); 518 | } ); 519 | tags = new wp.api.collections.Tags( newTags ); 520 | self.setTagsWithCollection( tags ); 521 | } 522 | } ); 523 | 524 | } else { 525 | this.setTagsWithCollection( tags ); 526 | } 527 | }, 528 | 529 | /** 530 | * Set the tags for a post. 531 | * 532 | * Accepts a Tags collection. 533 | * 534 | * @param {array|Backbone.Collection} tags The tags to set on the post. 535 | * 536 | */ 537 | setTagsWithCollection: function( tags ) { 538 | 539 | // Pluck out the category ids. 540 | this.set( 'tags', tags.pluck( 'id' ) ); 541 | return this.save(); 542 | } 543 | }, 544 | 545 | /** 546 | * Add a helper funtion to handle post Categories. 547 | */ 548 | CategoriesMixin = { 549 | 550 | /** 551 | * Get a the categories for a post. 552 | * 553 | * @return {Deferred.promise} promise Resolves to an array of categories. 554 | */ 555 | getCategories: function() { 556 | var categoryIds = this.get( 'categories' ), 557 | categories = new wp.api.collections.Categories(); 558 | 559 | // Resolve with an empty array if no categories. 560 | if ( _.isEmpty( categoryIds ) ) { 561 | return jQuery.Deferred().resolve( [] ); 562 | } 563 | 564 | return categories.fetch( { data: { include: categoryIds } } ); 565 | }, 566 | 567 | /** 568 | * Set the categories for a post. 569 | * 570 | * Accepts an array of category slugs, or a Categories collection. 571 | * 572 | * @param {array|Backbone.Collection} categories The categories to set on the post. 573 | * 574 | */ 575 | setCategories: function( categories ) { 576 | var allCategories, newCategory, 577 | self = this, 578 | newCategories = []; 579 | 580 | if ( _.isString( categories ) ) { 581 | return false; 582 | } 583 | 584 | // If this is an array of slugs, build a collection. 585 | if ( _.isArray( categories ) ) { 586 | 587 | // Get all the categories. 588 | allCategories = new wp.api.collections.Categories(); 589 | allCategories.fetch( { 590 | data: { per_page: 100 }, 591 | success: function( allcats ) { 592 | 593 | // Find the passed categories and set them up. 594 | _.each( categories, function( category ) { 595 | newCategory = new wp.api.models.Category( allcats.findWhere( { slug: category } ) ); 596 | 597 | // Tie the new category to the post. 598 | newCategory.set( 'parent_post', self.get( 'id' ) ); 599 | 600 | // Add the new category to the collection. 601 | newCategories.push( newCategory ); 602 | } ); 603 | categories = new wp.api.collections.Categories( newCategories ); 604 | self.setCategoriesWithCollection( categories ); 605 | } 606 | } ); 607 | 608 | } else { 609 | this.setCategoriesWithCollection( categories ); 610 | } 611 | 612 | }, 613 | 614 | /** 615 | * Set the categories for a post. 616 | * 617 | * Accepts Categories collection. 618 | * 619 | * @param {array|Backbone.Collection} categories The categories to set on the post. 620 | * 621 | */ 622 | setCategoriesWithCollection: function( categories ) { 623 | 624 | // Pluck out the category ids. 625 | this.set( 'categories', categories.pluck( 'id' ) ); 626 | return this.save(); 627 | } 628 | }, 629 | 630 | /** 631 | * Add a helper function to retrieve the author user model. 632 | */ 633 | AuthorMixin = { 634 | getAuthorUser: function() { 635 | return buildModelGetter( this, this.get( 'author' ), 'User', 'author', 'name' ); 636 | } 637 | }, 638 | 639 | /** 640 | * Add a helper function to retrieve the featured media. 641 | */ 642 | FeaturedMediaMixin = { 643 | getFeaturedMedia: function() { 644 | return buildModelGetter( this, this.get( 'featured_media' ), 'Media', 'wp:featuredmedia', 'source_url' ); 645 | } 646 | }; 647 | 648 | // Exit if we don't have valid model defaults. 649 | if ( _.isUndefined( model.prototype.args ) ) { 650 | return model; 651 | } 652 | 653 | // Go thru the parsable date fields, if our model contains any of them it gets the TimeStampedMixin. 654 | _.each( parseableDates, function( theDateKey ) { 655 | if ( ! _.isUndefined( model.prototype.args[ theDateKey ] ) ) { 656 | hasDate = true; 657 | } 658 | } ); 659 | 660 | // Add the TimeStampedMixin for models that contain a date field. 661 | if ( hasDate ) { 662 | model = model.extend( TimeStampedMixin ); 663 | } 664 | 665 | // Add the AuthorMixin for models that contain an author. 666 | if ( ! _.isUndefined( model.prototype.args.author ) ) { 667 | model = model.extend( AuthorMixin ); 668 | } 669 | 670 | // Add the FeaturedMediaMixin for models that contain a featured_media. 671 | if ( ! _.isUndefined( model.prototype.args.featured_media ) ) { 672 | model = model.extend( FeaturedMediaMixin ); 673 | } 674 | 675 | // Add the CategoriesMixin for models that support categories collections. 676 | if ( ! _.isUndefined( model.prototype.args.categories ) ) { 677 | model = model.extend( CategoriesMixin ); 678 | } 679 | 680 | // Add the MetaMixin for models that support meta collections. 681 | if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Meta' ] ) ) { 682 | model = model.extend( MetaMixin ); 683 | } 684 | 685 | // Add the TagsMixin for models that support tags collections. 686 | if ( ! _.isUndefined( model.prototype.args.tags ) ) { 687 | model = model.extend( TagsMixin ); 688 | } 689 | 690 | // Add the RevisionsMixin for models that support revisions collections. 691 | if ( ! _.isUndefined( loadingObjects.collections[ modelClassName + 'Revisions' ] ) ) { 692 | model = model.extend( RevisionsMixin ); 693 | } 694 | 695 | return model; 696 | }; 697 | 698 | })( window ); 699 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-api", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "postinstall": "bower install && grunt", 6 | "build": "grunt", 7 | "test": "grunt test" 8 | }, 9 | "devDependencies": { 10 | "bower": "~1.8.0", 11 | "grunt": "~1.0.1", 12 | "grunt-cli": "^1.2.0", 13 | "grunt-contrib-concat": "^1.0.1", 14 | "grunt-contrib-jshint": "^1.1.0", 15 | "grunt-contrib-qunit": "~1.3.0", 16 | "grunt-contrib-uglify": "2.1.0", 17 | "grunt-contrib-watch": "1.0.0", 18 | "grunt-jscs": "^3.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /routes-to-add.md: -------------------------------------------------------------------------------- 1 | Backbone.js App for WP-API 2 | ============== 3 | 4 | ## Route Work Tracking ## 5 | * Models tested GET from the following endpoints: 6 | - [ ] /: {} 7 | - [x] /wp/v2: {} 8 | - [ ] /wp/v2/posts: {} 9 | - [x] /wp/v2/posts/(?P[\d]+): {} 10 | - [ ] /wp/v2/posts/(?P[\d]+)/meta: {} 11 | - [ ] /wp/v2/posts/(?P[\d]+)/meta/(?P[\d]+): {} 12 | - [x] /wp/v2/posts/(?P[\d]+)/revisions: {} 13 | - [ ] /wp/v2/posts/(?P[\d]+)/revisions/(?P[\d]+): {} 14 | - [ ] /wp/v2/posts/(?P[\d]+)/terms/category: {} 15 | - [ ] /wp/v2/posts/(?P[\d]+)/terms/category/(?P[\d]+): {} 16 | - [ ] /wp/v2/posts/(?P[\d]+)/terms/tag: {} 17 | - [ ] /wp/v2/posts/(?P[\d]+)/terms/tag/(?P[\d]+): {} 18 | - [ ] /wp/v2/pages: {} 19 | - [x] /wp/v2/pages/(?P[\d]+): {} 20 | - [ ] /wp/v2/pages/(?P[\d]+)/meta: {} 21 | - [ ] /wp/v2/pages/(?P[\d]+)/meta/(?P[\d]+): {} 22 | - [ ] /wp/v2/pages/(?P[\d]+)/revisions: {} 23 | - [ ] /wp/v2/pages/(?P[\d]+)/revisions/(?P[\d]+): {} 24 | - [ ] /wp/v2/media: {} 25 | - [x] /wp/v2/media/(?P[\d]+): {} 26 | - [ ] /wp/v2/types: {} 27 | - [ ] /wp/v2/types/(?P[\w-]+): {} 28 | - [ ] /wp/v2/statuses: {} 29 | - [x] /wp/v2/statuses/(?P[\w-]+): {} 30 | - [ ] /wp/v2/taxonomies: {} 31 | - [x] /wp/v2/taxonomies/(?P[\w-]+): {} 32 | - [ ] /wp/v2/terms/category: {} 33 | - [ ] /wp/v2/terms/category/(?P[\d]+): {} 34 | - [ ] /wp/v2/terms/tag: {} 35 | - [x] /wp/v2/terms/tag/(?P[\d]+): {} 36 | - [ ] /wp/v2/users: {} 37 | - [x] /wp/v2/users/(?P[\d]+): {} 38 | - [x] /wp/v2/users/me: {} 39 | - [ ] /wp/v2/comments: {} 40 | - [x] /wp/v2/comments/(?P[\d]+): {} 41 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "asyncTest" : false, 4 | "deepEqual" : false, 5 | "equal" : false, 6 | "expect" : false, 7 | "module" : false, 8 | "notDeepEqual" : false, 9 | "notEqual" : false, 10 | "notStrictEqual" : false, 11 | "ok" : false, 12 | "QUnit" : false, 13 | "raises" : false, 14 | "sinon" : false, 15 | "start" : false, 16 | "stop" : false, 17 | "strictEqual" : false, 18 | "test" : false, 19 | "JSON" : false, 20 | "_" : false, 21 | "jQuery" : false, 22 | "wp" : false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/run-tests.js: -------------------------------------------------------------------------------- 1 | /* global console, wpApiSettings:false */ 2 | // Suppress warning about parse function's unused "options" argument: 3 | /* jshint unused:false */ 4 | (function( wp, wpApiSettings, window, undefined ) { 5 | /** 6 | * Test the endpoints. 7 | * 8 | * @todo add an assertion library. 9 | */ 10 | jQuery( document ).ready( function() { 11 | console.log( 'Running tests.' ); 12 | /** 13 | * Post 14 | */ 15 | var postTests = function() { 16 | console.log( 'running postTests' ); 17 | // Create a post. 18 | console.log( 'Create a post using wp.api.models.Post' ); 19 | var post = new wp.api.models.Posts(); 20 | var data = { 21 | title: 'This is a test post' 22 | }; 23 | 24 | var success = function( response ) { 25 | // Created the post. 26 | console.log ( 'Created post ID: ' + response.id ); 27 | 28 | // Try Fetching 29 | console.log( 'Fetching a post using wp.api.models.Post' ); 30 | data = { 31 | id: response.id 32 | }; 33 | 34 | var post2 = new wp.api.models.Posts( data ); 35 | post2.fetch( { 36 | success: function( model, response ) { 37 | 38 | // Fetch success. 39 | console.log ( 'Read post ID: ' + post2.get( 'id' ) ); 40 | 41 | // Try deleting. 42 | console.log ( 'Deleting post ID: ' + post2.get( 'id' ) ); 43 | post2.destroy( { 44 | success: function( model, response ) { 45 | // Delete success. 46 | console.log ( 'Deleted ' + model.get( 'id' ) ); 47 | 48 | // Check status, verify trashed. 49 | data = { 50 | id: response.id, 51 | post_status: 'trashed' 52 | }; 53 | var post3 = new wp.api.models.Post( data ); 54 | post3.fetch( { 55 | success: function( model, response, options ) { 56 | console.log ( 'Re-read post, status is: ' + model.get( 'post_status' ) ); 57 | 58 | // @todo Contunue tests. 59 | 60 | } 61 | } ); 62 | } 63 | } ); 64 | 65 | } 66 | } ); 67 | 68 | 69 | 70 | }; 71 | 72 | post.save( data, { success: success } ); 73 | }; 74 | postTests(); 75 | 76 | } ); 77 | })( wp, wpApiSettings, window ); 78 | -------------------------------------------------------------------------------- /tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CLIENT-JS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

WP-JS-Hooks

20 |

21 |
22 |

23 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/wp-api.js: -------------------------------------------------------------------------------- 1 | module( 'WP-API JS Client Tests' ); 2 | 3 | QUnit.test( 'API Loaded correctly', function( assert ) { 4 | var done = assert.async(); 5 | 6 | assert.expect( 2 ); 7 | assert.ok( wp.api.loadPromise ); 8 | 9 | wp.api.loadPromise.done( function() { 10 | assert.ok( wp.api.models ); 11 | done(); 12 | } ); 13 | 14 | } ); 15 | 16 | // Test custom namespaces are parsed correctly. 17 | wp.api.init( { 18 | 'versionString': 'js-widgets/v1/' 19 | } ) 20 | .done( function() { 21 | var customModels = [ 22 | 'WidgetsText', 23 | 'WidgetsRecentPosts', 24 | 'WidgetsPostCollection' 25 | ]; 26 | 27 | // Check that we have and can get each model type. 28 | _.each( customModels, function( className ) { 29 | QUnit.test( 'Checking ' + className + ' model.' , function( assert ) { 30 | var done = assert.async(); 31 | 32 | assert.expect( 2 ); 33 | 34 | wp.api.loadPromise.done( function() { 35 | var theModel = new wp.api.models[ className ](); 36 | assert.ok( theModel, 'We can instantiate wp.api.models.' + className ); 37 | theModel.fetch().done( function() { 38 | var theModel2 = new wp.api.models[ className ](); 39 | theModel2.set( 'id', theModel.attributes[0].id ); 40 | theModel2.fetch().done( function() { 41 | assert.equal( theModel.attributes[0].id, theModel2.get( 'id' ) , 'We should be able to get a ' + className ); 42 | done(); 43 | } ); 44 | } ); 45 | 46 | } ); 47 | 48 | }); 49 | } ); 50 | 51 | } ); 52 | 53 | 54 | // Verify collections loaded. 55 | var collectionClassNames = [ 56 | 'Categories', 57 | 'Comments', 58 | 'Media', 59 | 'Pages', 60 | 'Posts', 61 | 'Statuses', 62 | 'Tags', 63 | 'Taxonomies', 64 | 'Types', 65 | 'Users' 66 | ]; 67 | 68 | var collectionHelperTests = [ 69 | { 70 | 'collectionType': 'Posts', 71 | 'returnsModelType': 'post', 72 | 'supportsMethods': { 73 | 'getDate': 'getDate', 74 | 'getRevisions': 'getRevisions', 75 | 'getTags': 'getTags', 76 | 'getCategories': 'getCategories', 77 | 'getAuthorUser': 'getAuthorUser', 78 | 'getFeaturedMedia': 'getFeaturedMedia' 79 | /*'getMeta': 'getMeta', currently not supported */ 80 | } 81 | }, 82 | { 83 | 'collectionType': 'Pages', 84 | 'returnsModelType': 'page', 85 | 'supportsMethods': { 86 | 'getDate': 'getDate', 87 | 'getRevisions': 'getRevisions', 88 | 'getAuthorUser': 'getAuthorUser', 89 | 'getFeaturedMedia': 'getFeaturedMedia' 90 | } 91 | } 92 | ]; 93 | 94 | // Check that we have and can get each collection type. 95 | _.each( collectionClassNames, function( className ) { 96 | QUnit.test( 'Testing ' + className + ' collection.', function( assert ) { 97 | var done = assert.async(); 98 | 99 | wp.api.loadPromise.done( function() { 100 | var theCollection = new wp.api.collections[ className ](); 101 | assert.ok( 102 | theCollection, 103 | 'We can instantiate wp.api.collections.' + className 104 | ); 105 | theCollection.fetch().done( function() { 106 | assert.equal( 107 | 1, 108 | theCollection.state.currentPage, 109 | 'We should be on page 1 of the collection in ' + className 110 | ); 111 | 112 | // Should this collection have helper methods? 113 | var collectionHelperTest = _.findWhere( collectionHelperTests, { 'collectionType': className } ); 114 | 115 | // If we found a match, run the tests against it. 116 | if ( ! _.isUndefined( collectionHelperTest ) ) { 117 | 118 | // Test the first returned model. 119 | var firstModel = theCollection.at( 0 ); 120 | 121 | // Is the model the right type? 122 | assert.equal( 123 | collectionHelperTest.returnsModelType, 124 | firstModel.get( 'type' ), 125 | 'The wp.api.collections.' + className + ' is of type ' + collectionHelperTest.returnsModelType 126 | ); 127 | 128 | // Does the model have all of the expected supported methods? 129 | _.each( collectionHelperTest.supportsMethods, function( method ) { 130 | assert.equal( 131 | 'function', 132 | typeof firstModel[ method ], 133 | className + '.' + method + ' is a function.' 134 | ); 135 | } ); 136 | } 137 | 138 | 139 | done(); 140 | } ); 141 | 142 | } ); 143 | 144 | }); 145 | } ); 146 | 147 | var modelsWithIdsClassNames = 148 | [ 149 | 'Category', 150 | 'Media', 151 | 'Page', 152 | 'Post', 153 | 'Tag', 154 | 'User' 155 | ]; 156 | 157 | // Check that we have and can get each model type. 158 | _.each( modelsWithIdsClassNames, function( className ) { 159 | 160 | QUnit.test( 'Checking ' + className + ' model.' , function( assert ) { 161 | var done = assert.async(); 162 | 163 | assert.expect( 2 ); 164 | 165 | wp.api.loadPromise.done( function() { 166 | var theModel = new wp.api.models[ className ](); 167 | assert.ok( theModel, 'We can instantiate wp.api.models.' + className ); 168 | theModel.fetch().done( function() { 169 | var theModel2 = new wp.api.models[ className ](); 170 | theModel2.set( 'id', theModel.attributes[0].id ); 171 | theModel2.fetch().done( function() { 172 | assert.equal( theModel.attributes[0].id, theModel2.get( 'id' ) , 'We should be able to get a ' + className ); 173 | done(); 174 | } ); 175 | } ); 176 | 177 | } ); 178 | 179 | }); 180 | } ); 181 | 182 | var modelsWithIndexes = 183 | [ 184 | 'Taxonomy', 185 | 'Status', 186 | 'Type' 187 | ]; 188 | 189 | // Check that we have and can get each model type. 190 | _.each( modelsWithIndexes, function( className ) { 191 | 192 | QUnit.test( 'Testing ' + className + ' model.' , function( assert ) { 193 | var done = assert.async(); 194 | 195 | assert.expect( 2 ); 196 | 197 | wp.api.loadPromise.done( function() { 198 | var theModel = new wp.api.models[ className ](); 199 | assert.ok( theModel, 'We can instantiate wp.api.models.' + className ); 200 | theModel.fetch().done( function() { 201 | var theModel2 = new wp.api.models[ className ](); 202 | 203 | if ( ! _.isUndefined( theModel.attributes[0] ) ) { 204 | theModel2.set( 'id', theModel.attributes[0].id ); 205 | } 206 | 207 | theModel2.fetch().done( function() { 208 | assert.notEqual( 0, _.keys( theModel2.attributes ).length , 'We should be able to get a ' + className ); 209 | done(); 210 | } ); 211 | } ); 212 | 213 | } ); 214 | 215 | }); 216 | } ); 217 | 218 | // Check that getAuthorUser handles errors when the callback fails. 219 | QUnit.test( 'Testing getAuthorUser ajax failure.' , function( assert ) { 220 | var done = assert.async(); 221 | 222 | assert.expect( 1 ); 223 | 224 | wp.api.loadPromise.done( function() { 225 | var post = new wp.api.models.Post( { 'id': 1 } ); 226 | post.fetch().done( function() { 227 | 228 | var originalFetch = window.Backbone.Model.prototype.fetch; 229 | 230 | // Override Backbone.Model.fetch to force an error. 231 | window.Backbone.Model.prototype.fetch = function( options ) { 232 | var deferred = jQuery.Deferred(), 233 | promise = deferred.promise(); 234 | 235 | if ( options.error ) { 236 | assert.equal( 1, 1 , 'getAuthorUser should have error callback on failure.' ); 237 | done(); 238 | } else { 239 | assert.equal( 1, 0 , 'getAuthorUser should have error callback on failure.' ); 240 | done(); 241 | } 242 | 243 | deferred.reject(); 244 | return promise; 245 | }; 246 | 247 | post.getAuthorUser(); 248 | window.Backbone.Model.prototype.fetch = originalFetch; 249 | } ); 250 | } ); 251 | } ); 252 | --------------------------------------------------------------------------------