├── .david-dev
├── .jshintrc
├── phpunit.xml.dist
├── .gitmodules
├── .jshintignore
├── .coveralls.yml
├── .eslintignore
├── .dev-lib
├── .gitignore
├── post-collection-widget
├── view.css
├── form.css
├── class-plugin.php
├── form.js
└── class-widget.php
├── .jscsrc
├── core-adapter-widgets
├── rss
│ ├── form.js
│ └── class.php
├── meta
│ ├── form.js
│ └── class.php
├── search
│ ├── form.js
│ └── class.php
├── archives
│ ├── form.js
│ └── class.php
├── calendar
│ ├── form.js
│ └── class.php
├── tag_cloud
│ ├── form.js
│ └── class.php
├── categories
│ ├── form.js
│ └── class.php
├── recent-posts
│ ├── form.js
│ └── class.php
├── recent-comments
│ ├── form.js
│ └── class.php
├── pages
│ ├── form.js
│ └── class.php
└── nav_menu
│ ├── class.php
│ └── form.js
├── composer.json
├── phpcs.ruleset.xml
├── .travis.yml
├── js
├── trac-39389-controls.js
├── shortcode-ui-view-widget-form-field.js
├── trac-39389-preview.js
├── admin-js-widgets.js
├── customize-js-widgets.js
└── widget-form.js
├── php
├── class-wp-customize-js-widget-control.php
├── class-wp-adapter-js-widget.php
├── class-js-widget-shortcode-controller.php
├── class-js-widgets-shortcode-ui.php
└── class-js-widgets-rest-controller.php
├── package.json
├── js-widgets.php
├── post-collection-widget.php
├── tests
└── js
│ └── customize-js-widgets.js
├── Gruntfile.js
├── contributing.md
├── css
└── widget-form.css
├── .eslintrc
├── readme.txt
└── readme.md
/.david-dev:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | dev-lib/.jshintrc
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 | dev-lib/phpunit-plugin.xml
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "dev-lib"]
2 | path = dev-lib
3 | url = https://github.com/xwp/wp-dev-lib.git
4 | branch = master
5 |
--------------------------------------------------------------------------------
/.jshintignore:
--------------------------------------------------------------------------------
1 | **/*.min.js
2 | **/node_modules/**
3 | **/vendor/**
4 | **/*.jsx
5 | /bower_components/**
6 | /tests/js/**
7 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 | coverage_clover: build/logs/clover.xml
3 | json_path: build/logs/coveralls-upload.json
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.min.js
2 | **/node_modules/**
3 | **/vendor/**
4 | /tests/js/lib/**
5 | **/*.browserified.js
6 | /bower_components/**
7 |
--------------------------------------------------------------------------------
/.dev-lib:
--------------------------------------------------------------------------------
1 | WPCS_GIT_TREE=develop
2 | ASSETS_DIR=wp-assets
3 |
4 | if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.2" ]] || [[ ${TRAVIS_PHP_VERSION:0:3} == "5.3" ]]; then
5 | DEV_LIB_SKIP="$DEV_LIB_SKIP,phpcs"
6 | fi
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Grunt
4 | /build/
5 | /node_modules/
6 | npm-debug.log
7 |
8 | # Composer
9 | composer.lock
10 | /vendor/
11 |
12 | # Compiled files
13 | *.min.js
14 | *.min.css
15 |
--------------------------------------------------------------------------------
/post-collection-widget/view.css:
--------------------------------------------------------------------------------
1 |
2 | .widget-post-collection-list .title,
3 | .widget-post-collection-list .author {
4 | margin: 0;
5 | }
6 | .widget-post-collection-list .date {
7 | display: block;
8 | }
9 | .widget-post-collection-list li {
10 | margin-bottom: 0.5em;
11 | }
12 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "wordpress",
3 | "requireCamelCaseOrUpperCaseIdentifiers": {
4 | "ignoreProperties": true,
5 | "allExcept": [ "Shortcode_UI" ]
6 | },
7 | "excludeFiles": [
8 | "**/*.min.js",
9 | "**/*.jsx",
10 | "**/node_modules/**",
11 | "**/vendor/**",
12 | "**/tests/**",
13 | "bower_components/**"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/post-collection-widget/form.css:
--------------------------------------------------------------------------------
1 |
2 | .widget-post-collection .customize-object-selector-container,
3 | .widget-post-collection .customize-object-selector-container .select2-container {
4 | display: block;
5 | }
6 |
7 | .widget-post-collection .customize-object-selector-container .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
8 | display: block;
9 | float: none;
10 | cursor: move;
11 | }
12 |
--------------------------------------------------------------------------------
/core-adapter-widgets/rss/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.rss = (function() {
7 | 'use strict';
8 |
9 | var RSSWidgetForm;
10 |
11 | /**
12 | * RSS Widget Form.
13 | *
14 | * @constructor
15 | */
16 | RSSWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = RSSWidgetForm;
20 | }
21 | return RSSWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/meta/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.meta = (function() {
7 | 'use strict';
8 |
9 | var MetaWidgetForm;
10 |
11 | /**
12 | * Meta Widget Form.
13 | *
14 | * @constructor
15 | */
16 | MetaWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = MetaWidgetForm;
20 | }
21 | return MetaWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/search/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.search = (function() {
7 | 'use strict';
8 |
9 | var SearchWidgetForm;
10 |
11 | /**
12 | * Search Widget Form.
13 | *
14 | * @constructor
15 | */
16 | SearchWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = SearchWidgetForm;
20 | }
21 | return SearchWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/archives/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.archives = (function() {
7 | 'use strict';
8 |
9 | var ArchivesWidgetForm;
10 |
11 | /**
12 | * Archives Widget Form.
13 | *
14 | * @constructor
15 | */
16 | ArchivesWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = ArchivesWidgetForm;
20 | }
21 | return ArchivesWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/calendar/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.calendar = (function() {
7 | 'use strict';
8 |
9 | var CalendarWidgetForm;
10 |
11 | /**
12 | * Calendar Widget Form.
13 | *
14 | * @constructor
15 | */
16 | CalendarWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = CalendarWidgetForm;
20 | }
21 | return CalendarWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/tag_cloud/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.tag_cloud = (function() {
7 | 'use strict';
8 |
9 | var TagCloudWidgetForm;
10 |
11 | /**
12 | * Tag Cloud Widget Form.
13 | *
14 | * @constructor
15 | */
16 | TagCloudWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = TagCloudWidgetForm;
20 | }
21 | return TagCloudWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/categories/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor.categories = (function() {
7 | 'use strict';
8 |
9 | var CategoriesWidgetForm;
10 |
11 | /**
12 | * Categories Widget Form.
13 | *
14 | * @constructor
15 | */
16 | CategoriesWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = CategoriesWidgetForm;
20 | }
21 | return CategoriesWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/recent-posts/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor['recent-posts'] = (function() {
7 | 'use strict';
8 |
9 | var RecentPostsWidgetForm;
10 |
11 | /**
12 | * Recent Posts Widget Form.
13 | *
14 | * @constructor
15 | */
16 | RecentPostsWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = RecentPostsWidgetForm;
20 | }
21 | return RecentPostsWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/core-adapter-widgets/recent-comments/form.js:
--------------------------------------------------------------------------------
1 | /* global wp, module */
2 | /* eslint consistent-this: [ "error", "form" ] */
3 | /* eslint-disable strict */
4 | /* eslint-disable complexity */
5 |
6 | wp.widgets.formConstructor['recent-comments'] = (function() {
7 | 'use strict';
8 |
9 | var RecentCommentsWidgetForm;
10 |
11 | /**
12 | * Recent Comments Widget Form.
13 | *
14 | * @constructor
15 | */
16 | RecentCommentsWidgetForm = wp.widgets.Form.extend( {} );
17 |
18 | if ( 'undefined' !== typeof module ) {
19 | module.exports = RecentCommentsWidgetForm;
20 | }
21 | return RecentCommentsWidgetForm;
22 |
23 | })();
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xwp/wp-js-widgets",
3 | "description": "The next generation of widgets in Core (Widgets 3.0), embracing JS for UI and powering the Widgets REST API.",
4 | "version": "0.4.3",
5 | "type": "wordpress-plugin",
6 | "keywords": [ "customizer", "widgets", "rest-api" ],
7 | "homepage": "https://github.com/xwp/wp-js-widgets/",
8 | "license": [ "GPL-2.0+" ],
9 | "repositories": [
10 | {
11 | "type": "git",
12 | "url": "https://github.com/xwp/wp-js-widgets.git"
13 | }
14 | ],
15 | "dist": {
16 | "url": "https://downloads.wordpress.org/plugin/js-widgets.zip",
17 | "type": "zip"
18 | },
19 | "require": {
20 | "php": ">=5.2.0"
21 | },
22 | "authors": [
23 | {
24 | "name": "Weston Ruter",
25 | "homepage": "https://weston.ruter.net/"
26 | }
27 | ],
28 | "require-dev": {
29 | "satooshi/php-coveralls": "dev-master"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/phpcs.ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
408 | %2$s', 414 | 'https://github.com/xwp/wp-customize-object-selector', 415 | __( 'Customize Object Selector', 'js-widgets' ) 416 | ) 417 | ) ); 418 | ?> 419 |
420 | 421 | render_title_form_field_template(); 423 | ?> 424 |425 | 426 | 427 |
428 | render_form_field_template( array( 430 | 'field' => 'show_date', 431 | 'label' => __( 'Show date', 'js-widgets' ), 432 | ) ); 433 | $this->render_form_field_template( array( 434 | 'field' => 'show_author', 435 | 'label' => __( 'Show author', 'js-widgets' ), 436 | ) ); 437 | $this->render_form_field_template( array( 438 | 'field' => 'show_featured_image', 439 | 'label' => __( 'Show featured image', 'js-widgets' ), 440 | ) ); 441 | ?> 442 | 0 ) 78 | .then( function() { 79 | if ( notifications.length > 0 ) { 80 | container.css( 'height', 'auto' ); 81 | } 82 | } ); 83 | form.container.toggleClass( 'has-error', notifications.filter( isNotificationError ).length > 0 ); 84 | form.container.toggleClass( 'has-notifications', notifications.length > 0 ); 85 | 86 | notifications.map( speakNotification ); 87 | 88 | templateFunction = getNotificationsTemplate( form ); 89 | renderMarkupToContainer( container, templateFunction( { notifications: notifications, altNotice: Boolean( form.altNotice ) } ) ); 90 | }, 91 | 92 | /** 93 | * Get the element inside of a form's container that contains the notifications. 94 | * 95 | * Control subclasses may override this to return the proper container to render notifications into. 96 | * 97 | * @returns {jQuery} Notifications container element. 98 | */ 99 | getNotificationsContainerElement: function getNotificationsContainerElement() { 100 | var form = this; 101 | return form.container.find( '.js-widget-form-notifications-container:first' ); 102 | }, 103 | 104 | /** 105 | * Validate the instance data. 106 | * 107 | * @todo In order for returning an error/notification to work properly, api._handleSettingValidities needs to only remove notification errors that are no longer valid which are fromServer: 108 | * 109 | * @param {object} value Instance value. 110 | * @returns {object|Error|wp.customize.Notification} Sanitized instance value or error/notification. 111 | */ 112 | validate: function validate( value ) { 113 | var form = this, newValue, oldValue; 114 | oldValue = form.model.get(); 115 | newValue = form.sanitize( value, oldValue ); 116 | 117 | // Remove all existing notifications added via sanitization since only one can be returned. 118 | removeSanitizeNotifications( form.notifications ); 119 | 120 | // If sanitize method returned an error/notification, block setting update and add a notification 121 | if ( newValue instanceof Error ) { 122 | newValue = new api.Notification( 'invalidValue', { message: newValue.message, type: 'error' } ); 123 | } 124 | if ( newValue instanceof api.Notification ) { 125 | addSanitizeNotification( form, newValue ); 126 | return null; 127 | } 128 | 129 | return newValue; 130 | }, 131 | 132 | /** 133 | * Sanitize the instance data. 134 | * 135 | * @param {object} newInstance New instance. 136 | * @param {object} oldInstance Existing instance. 137 | * @returns {object|Error|wp.customize.Notification} Sanitized instance or validation error/notification. 138 | */ 139 | sanitize: function sanitize( newInstance, oldInstance ) { 140 | var form = this, instance, code, notification; 141 | if ( _.isUndefined( oldInstance ) ) { 142 | throw new Error( 'Expected oldInstance' ); 143 | } 144 | instance = _.extend( {}, newInstance ); 145 | 146 | // Warn about markup in title. 147 | code = 'markupTitleInvalid'; 148 | if ( /<\/?\w+[^>]*>/.test( instance.title ) ) { 149 | notification = new api.Notification( code, { 150 | message: form.config.l10n.title_tags_invalid, 151 | type: 'warning' 152 | } ); 153 | form.notifications.add( code, notification ); 154 | } else { 155 | form.notifications.remove( code ); 156 | } 157 | 158 | /* 159 | * Trim per sanitize_text_field(). 160 | * Protip: This prevents the widget partial from refreshing after adding a space or adding a new paragraph. 161 | */ 162 | if ( instance.title ) { 163 | instance.title = $.trim( instance.title ); 164 | } 165 | 166 | return instance; 167 | }, 168 | 169 | /** 170 | * Get cloned value. 171 | * 172 | * @todo This will only do shallow copy. 173 | * 174 | * @return {object} Instance. 175 | */ 176 | getValue: function getValue() { 177 | var form = this; 178 | return _.extend( 179 | {}, 180 | form.config.default_instance, 181 | form.model.get() || {} 182 | ); 183 | }, 184 | 185 | /** 186 | * Merge the props into the current value. 187 | * 188 | * @todo Rename this update? Rename this set? Or setExtend? Or setValue()? 189 | * 190 | * @param {object} props Instance props. 191 | * @returns {void} 192 | */ 193 | setState: function setState( props ) { 194 | var form = this, validated; 195 | validated = form.validate( _.extend( {}, form.model.get(), props ) ); 196 | if ( ! validated || validated instanceof Error || validated instanceof api.Notification ) { 197 | return; 198 | } 199 | form.model.set( validated ); 200 | }, 201 | 202 | /** 203 | * Create synced property value. 204 | * 205 | * Given that the current setting contains an object value, create a new 206 | * model (Value) to represent the value of one of its properties, and 207 | * sync the value between the root object and the property value when 208 | * either are changed. The returned Value can be used to sync with an 209 | * Element. 210 | * 211 | * @param {wp.customize.Value} root Root value instance. 212 | * @param {string} property Property name. 213 | * @returns {object} Property value instance. 214 | */ 215 | createSyncedPropertyValue: function createSyncedPropertyValue( root, property ) { 216 | var form = this, propertyValue, rootChangeListener, propertyChangeListener; 217 | 218 | propertyValue = new api.Value( form.getValue()[ property ] ); 219 | 220 | // Sync changes to the property back to the root value. 221 | propertyChangeListener = function( newPropertyValue ) { 222 | var newState = {}; 223 | newState[ property ] = newPropertyValue; 224 | form.setState( newState ); 225 | }; 226 | propertyValue.bind( propertyChangeListener ); 227 | 228 | // Sync changes in the root value to the model. 229 | rootChangeListener = function updateRootValue( newRootValue, oldRootValue ) { 230 | if ( ! _.isEqual( newRootValue[ property ], oldRootValue[ property ] ) ) { 231 | propertyValue.set( newRootValue[ property ] ); 232 | } 233 | }; 234 | root.bind( rootChangeListener ); 235 | 236 | return { 237 | value: propertyValue, 238 | propertyChangeListener: propertyChangeListener, 239 | rootChangeListener: rootChangeListener 240 | }; 241 | }, 242 | 243 | /** 244 | * Create elements to link setting value properties with corresponding inputs in the form. 245 | * 246 | * @returns {void} 247 | */ 248 | linkPropertyElements: function linkPropertyElements() { 249 | var form = this, initialInstanceData; 250 | initialInstanceData = form.getValue(); 251 | form.syncedProperties = {}; 252 | form.container.find( ':input[data-field]' ).each( function() { 253 | var input = $( this ), field = input.data( 'field' ), syncedProperty; 254 | if ( _.isUndefined( initialInstanceData[ field ] ) ) { 255 | return; 256 | } 257 | 258 | syncedProperty = form.createSyncedPropertyValue( form.model, field ); 259 | syncedProperty.element = new api.Element( input ); 260 | syncedProperty.element.set( initialInstanceData[ field ] ); 261 | syncedProperty.element.sync( syncedProperty.value ); 262 | form.syncedProperties[ field ] = syncedProperty; 263 | } ); 264 | }, 265 | 266 | /** 267 | * Unlink setting value properties with corresponding inputs in the form. 268 | * 269 | * @returns {void} 270 | */ 271 | unlinkPropertyElements: function unlinkPropertyElements() { 272 | var form = this; 273 | _.each( form.syncedProperties, function( syncedProperty ) { 274 | syncedProperty.element.unsync( syncedProperty.value ); 275 | form.model.unbind( syncedProperty.rootChangeListener ); 276 | syncedProperty.value.callbacks.remove(); 277 | } ); 278 | form.syncedProperties = {}; 279 | }, 280 | 281 | /** 282 | * Get template function. 283 | * 284 | * @returns {Function} Template function. 285 | */ 286 | getTemplate: function getTemplate() { 287 | var form = this; 288 | if ( ! form._template ) { 289 | if ( ! $( '#tmpl-' + form.config.form_template_id ).is( 'script[type="text/template"]' ) ) { 290 | throw new Error( 'Missing script[type="text/template"]#' + form.config.form_template_id + ' script for widget form.' ); 291 | } 292 | form._template = wp.template( form.config.form_template_id ); 293 | } 294 | return form._template; 295 | }, 296 | 297 | /** 298 | * Embed. 299 | * 300 | * @deprecated 301 | * @returns {void} 302 | */ 303 | embed: function embed() { 304 | if ( 'undefined' !== typeof console ) { 305 | console.warn( 'wp.widgets.Form#embed is deprecated.' ); 306 | } 307 | this.render(); 308 | }, 309 | 310 | /** 311 | * Render (mount) the form into the container. 312 | * 313 | * @returns {void} 314 | */ 315 | render: function render() { 316 | var form = this, template = form.getTemplate(); 317 | form.container.html( template( form ) ); 318 | form.linkPropertyElements(); 319 | form.notifications.bind( 'add', form.renderNotifications ); 320 | form.notifications.bind( 'remove', form.renderNotifications ); 321 | }, 322 | 323 | /** 324 | * Destruct (unrender/unmount) the form. 325 | * 326 | * Subclasses can do cleanup of event listeners on other components, 327 | * 328 | * @returns {void} 329 | */ 330 | destruct: function destruct() { 331 | var form = this; 332 | form.container.empty(); 333 | form.unlinkPropertyElements(); 334 | form.notifications.unbind( 'add', form.renderNotifications ); 335 | form.notifications.unbind( 'remove', form.renderNotifications ); 336 | } 337 | }); 338 | 339 | /** 340 | * Return an Array of an api.Values object's values 341 | * 342 | * @param {wp.customize.Values} values An instance of api.Values 343 | * @return {Array} An array of api.Value objects 344 | */ 345 | function getArrayFromValues( values ) { 346 | var ary = []; 347 | values.each( function( value ) { 348 | ary.push( value ); 349 | } ); 350 | return ary; 351 | } 352 | 353 | /** 354 | * Return true if the Notification is an error 355 | * 356 | * @param {wp.customize.Notification} notification An instance of api.Notification 357 | * @return {Boolean} True if the `type` of the Notification is 'error' 358 | */ 359 | function isNotificationError( notification ) { 360 | return 'error' === notification.type; 361 | } 362 | 363 | /** 364 | * Hide or show a DOM node using jQuery animation 365 | * 366 | * @param {jQuery} container The jQuery object to hide or show 367 | * @param {Boolean} showContainer True to show the node, or false to hide it 368 | * @return {Deferred} A promise that is resolved when the animation is complete 369 | */ 370 | function toggleContainer( container, showContainer ) { 371 | var deferred = $.Deferred(); 372 | if ( showContainer ) { 373 | container.stop().slideDown( 'fast', null, function() { 374 | deferred.resolve(); 375 | } ); 376 | } else { 377 | container.stop().slideUp( 'fast', null, deferred.resolve ); 378 | } 379 | return deferred; 380 | } 381 | 382 | /** 383 | * Speak a Notification using wp.a11y 384 | * 385 | * Will only speak a Notification once, so if passed a Notification that has already been spoken, this is a noop. 386 | * 387 | * @param {Notification} notification The Notification to speak 388 | * @return {void} 389 | */ 390 | function speakNotification( notification ) { 391 | if ( ! notification.hasA11ySpoken ) { 392 | 393 | // @todo In the context of the Customizer, this presently will end up getting spoken twice due to wp.customize.Control also rendering it. 394 | wp.a11y.speak( notification.message, 'assertive' ); 395 | notification.hasA11ySpoken = true; 396 | } 397 | } 398 | 399 | /** 400 | * Return the template function for rendering Notifications 401 | * 402 | * @param {Form} widgetForm The instance of the Form whose template to fetch 403 | * @return {Function} The template function 404 | */ 405 | function getNotificationsTemplate( widgetForm ) { 406 | if ( ! widgetForm._notificationsTemplate ) { 407 | widgetForm._notificationsTemplate = wp.template( widgetForm.config.notifications_template_id ); 408 | } 409 | return widgetForm._notificationsTemplate; 410 | } 411 | 412 | /** 413 | * Replace the markup of a DOM node container 414 | * 415 | * @param {jQuery} container The DOM node which will be replaced by the markup 416 | * @param {string} markup The markup to apply to the container 417 | * @return {void} 418 | */ 419 | function renderMarkupToContainer( container, markup ) { 420 | container.empty().append( $.trim( markup ) ); 421 | } 422 | 423 | /** 424 | * Removes Notification objects which have been added by `addSanitizeNotification` 425 | * 426 | * Note: this mutates the object itself! 427 | * 428 | * @param {wp.customize.Values} notifications An instance of api.Values containing Notification objects 429 | * @return {void} 430 | */ 431 | function removeSanitizeNotifications( notifications ) { 432 | notifications.each( function iterateNotifications( notification ) { 433 | if ( notification.viaWidgetFormSanitizeReturn ) { 434 | notifications.remove( notification.code ); 435 | } 436 | } ); 437 | } 438 | 439 | /** 440 | * Adds a Notification to a Form from the form's `sanitize` method 441 | * 442 | * @param {Form} widgetForm The instance of the Form to modify 443 | * @param {wp.customize.Values} notification An instance of api.Notification to add 444 | * @return {void} 445 | */ 446 | function addSanitizeNotification( widgetForm, notification ) { 447 | notification.viaWidgetFormSanitizeReturn = true; 448 | widgetForm.notifications.add( notification.code, notification ); 449 | } 450 | 451 | /** 452 | * Validate the properties of a Form 453 | * 454 | * Throws an Error if the properties are invalid. 455 | * 456 | * @param {Form} widgetForm The instance of the Form to modify 457 | * @return {void} 458 | */ 459 | function assertValidForm( widgetForm ) { 460 | if ( ! widgetForm.model || ! widgetForm.model.extended || ! widgetForm.model.extended( api.Value ) ) { 461 | throw new Error( 'Widget Form is missing model property which must be a Value or Setting instance.' ); 462 | } 463 | if ( 0 === widgetForm.container.length ) { 464 | throw new Error( 'Widget Form is missing container property as Element or jQuery.' ); 465 | } 466 | if ( ! widgetForm.config || ! widgetForm.config.default_instance ) { 467 | throw new Error( 'Widget Form class is missing config.default_instance' ); 468 | } 469 | } 470 | 471 | /** 472 | * Merges properties for a Form with the defaults 473 | * 474 | * The passed properties override the Form's config property which overrides the default values. 475 | * 476 | * @param {object} config The Form's current config property 477 | * @param {object} properties The passed-in properties to the Form 478 | * @return {object} The merged properties object 479 | */ 480 | function getValidatedFormProperties( config, properties ) { 481 | var defaultConfig = { 482 | form_template_id: '', 483 | notifications_template_id: '', 484 | l10n: {}, 485 | default_instance: {} 486 | }; 487 | 488 | var defaultProperties = { 489 | model: null, 490 | container: null, 491 | config: {} 492 | }; 493 | 494 | var formArguments = properties ? { model: properties.model, container: properties.container } : {}; 495 | var validProperties = _.extend( {}, defaultProperties, formArguments ); 496 | validProperties.config = _.extend( {}, defaultConfig, config ); 497 | return validProperties; 498 | } 499 | 500 | } )( wp.customize, jQuery, _ ); 501 | 502 | if ( 'undefined' !== typeof module ) { 503 | module.exports = wp.widgets.Form; 504 | } 505 | -------------------------------------------------------------------------------- /php/class-js-widgets-rest-controller.php: -------------------------------------------------------------------------------- 1 | plugin = $plugin; 37 | $this->namespace = $plugin->rest_api_namespace; 38 | $this->widget = $widget; 39 | $this->rest_base = $widget->id_base; 40 | } 41 | 42 | /** 43 | * Get namespace. 44 | * 45 | * @return string 46 | */ 47 | public function get_namespace() { 48 | return $this->namespace; 49 | } 50 | 51 | /** 52 | * Get REST Base. 53 | * 54 | * @return string 55 | */ 56 | public function get_rest_base() { 57 | return $this->rest_base; 58 | } 59 | 60 | /** 61 | * Get the object type for the REST resource. 62 | * 63 | * @return string 64 | */ 65 | protected function get_object_type() { 66 | return $this->widget->id_base . '-widget'; 67 | } 68 | 69 | /** 70 | * Get a widget object (resource) ID. 71 | * 72 | * This simple re-uses a widget number as a widget ID, which will only be unique 73 | * among the widgets of a given type. Eventually this ID should map to the post ID 74 | * for a given widget_instance post type so that it is truly unique across all 75 | * widget types in a site. 76 | * 77 | * @param int $widget_number Widget number. 78 | * @return int Widget object ID. 79 | */ 80 | protected function get_object_id( $widget_number ) { 81 | $widget_id = intval( $widget_number ); 82 | return $widget_id; 83 | } 84 | 85 | /** 86 | * Get the item's schema, conforming to JSON Schema. 87 | * 88 | * @return array 89 | */ 90 | public function get_item_schema() { 91 | $item_schema = array( 92 | '$schema' => 'http://json-schema.org/draft-04/schema#', 93 | 'title' => $this->get_object_type(), 94 | 'type' => 'object', 95 | 'properties' => array( 96 | 'id' => array( 97 | 'description' => __( 'Widget ID. This will only be unique among widgets of a given type until widgets are stored as posts. See WP Trac #35669.', 'js-widgets' ), 98 | 'type' => 'integer', 99 | 'context' => array( 'view', 'edit', 'embed' ), 100 | 'readonly' => true, 101 | ), 102 | 'type' => array( 103 | 'description' => __( 'Object type.', 'js-widgets' ), 104 | 'type' => 'string', 105 | 'context' => array( 'view', 'edit', 'embed' ), 106 | 'readonly' => true, 107 | ), 108 | ), 109 | ); 110 | 111 | $reserved_field_ids = array( 'id', 'type', '_links', '_embedded' ); 112 | 113 | foreach ( $this->widget->get_item_schema() as $field_id => $field_schema ) { 114 | 115 | // Prevent clobbering reserved fields. 116 | if ( in_array( $field_id, $reserved_field_ids, true ) ) { 117 | /* translators: %s is field ID */ 118 | _doing_it_wrong( get_class( $this->widget ) . '::get_item_schema', sprintf( __( 'The field "%s" is reserved.', 'js-widgets' ), esc_html( $field_id ) ), '' ); // WPCS: xss ok. 119 | continue; 120 | } 121 | 122 | // By default, widget properties are private and only available in an edit context. 123 | if ( ! isset( $field_schema['context'] ) ) { 124 | $field_schema['context'] = array( 'edit' ); 125 | } 126 | 127 | $item_schema['properties'][ $field_id ] = $field_schema; 128 | } 129 | 130 | $item_schema = $this->add_additional_fields_schema( $item_schema ); 131 | 132 | // Expose root-level required properties according to JSON Schema. 133 | if ( ! isset( $item_schema['required'] ) ) { 134 | $item_schema['required'] = array(); 135 | } 136 | foreach ( $item_schema['properties'] as $field_id => $field_schema ) { 137 | if ( ! empty( $field_schema['required'] ) ) { 138 | $item_schema['required'][] = $field_id; 139 | } 140 | } 141 | 142 | return $item_schema; 143 | } 144 | 145 | /** 146 | * Get base URL for API. 147 | * 148 | * @return string 149 | */ 150 | public function get_base_url() { 151 | $base = sprintf( '/%s/widgets/%s', $this->namespace, $this->rest_base ); 152 | return $base; 153 | } 154 | 155 | /** 156 | * Register routes. 157 | */ 158 | public function register_routes() { 159 | $route = '/widgets/' . $this->rest_base; 160 | 161 | register_rest_route( $this->namespace, $route, array( 162 | array( 163 | 'methods' => WP_REST_Server::READABLE, 164 | 'callback' => array( $this, 'get_items' ), 165 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 166 | 'args' => $this->get_collection_params(), 167 | ), 168 | array( 169 | 'methods' => WP_REST_Server::CREATABLE, 170 | 'callback' => array( $this, 'create_item' ), 171 | 'permission_callback' => array( $this, 'create_item_permissions_check' ), 172 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 173 | ), 174 | 'schema' => array( $this, 'get_public_item_schema' ), 175 | ) ); 176 | 177 | $route = '/widgets/' . $this->rest_base . '/(?P