diff --git a/inc/class-shortcode-ui.php b/inc/class-shortcode-ui.php index 0554b1c3..b6cb525d 100644 --- a/inc/class-shortcode-ui.php +++ b/inc/class-shortcode-ui.php @@ -96,7 +96,11 @@ public function enqueue() { usort( $shortcodes, array( $this, 'compare_shortcodes_by_label' ) ); - wp_enqueue_script( 'shortcode-ui', $this->plugin_url . 'js/build/shortcode-ui.js', array( 'jquery', 'backbone', 'mce-view' ), $this->plugin_version ); + // Load minified version of wp-js-hooks if not debugging. + $wp_js_hooks_file = 'wp-js-hooks' . ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '.min' : '' ) . '.js'; + + wp_enqueue_script( 'shortcode-ui-js-hooks', $this->plugin_url . 'lib/wp-js-hooks/' . $wp_js_hooks_file, array(), '2015-03-19' ); + wp_enqueue_script( 'shortcode-ui', $this->plugin_url . 'js/build/shortcode-ui.js', array( 'jquery', 'backbone', 'mce-view', 'shortcode-ui-js-hooks' ), $this->plugin_version ); wp_enqueue_style( 'shortcode-ui', $this->plugin_url . 'css/shortcode-ui.css', array(), $this->plugin_version ); wp_localize_script( 'shortcode-ui', ' shortcodeUIData', array( 'shortcodes' => $shortcodes, diff --git a/js-tests/build/specs.js b/js-tests/build/specs.js index b1291abd..2d815723 100644 --- a/js-tests/build/specs.js +++ b/js-tests/build/specs.js @@ -40,7 +40,7 @@ describe( "Shortcode Attribute Model", function() { description: 'test description', meta: { placeholder: 'test placeholder' - } + }, }; var attr = new ShortcodeAttribute( attrData ); @@ -361,7 +361,7 @@ var ShortcodeAttribute = Backbone.Model.extend({ description: '', meta: { placeholder: '', - } + }, }, }); diff --git a/js-tests/src/shortcodeAttributeModelSpec.js b/js-tests/src/shortcodeAttributeModelSpec.js index f9088700..b0a2d744 100644 --- a/js-tests/src/shortcodeAttributeModelSpec.js +++ b/js-tests/src/shortcodeAttributeModelSpec.js @@ -10,7 +10,7 @@ describe( "Shortcode Attribute Model", function() { description: 'test description', meta: { placeholder: 'test placeholder' - } + }, }; var attr = new ShortcodeAttribute( attrData ); diff --git a/js/build/field-attachment.js b/js/build/field-attachment.js index a23bda14..50321975 100644 --- a/js/build/field-attachment.js +++ b/js/build/field-attachment.js @@ -220,7 +220,7 @@ var ShortcodeAttribute = Backbone.Model.extend({ description: '', meta: { placeholder: '', - } + }, }, }); @@ -342,9 +342,9 @@ module.exports = window.Shortcode_UI; },{"./../collections/shortcodes.js":2}],8:[function(require,module,exports){ (function (global){ -var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), -sui = require('./../utils/sui.js'), -$ = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), + sui = require('./../utils/sui.js'), + $ = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); var editAttributeField = Backbone.View.extend( { @@ -391,6 +391,7 @@ var editAttributeField = Backbone.View.extend( { data.meta = _meta.join( ' ' ); this.$el.html( this.template( data ) ); + this.updateValue(); return this }, @@ -399,7 +400,8 @@ var editAttributeField = Backbone.View.extend( { * Input Changed Update Callback. * * If the input field that has changed is for content or a valid attribute, - * then it should update the model. + * then it should update the model. If a callback function is registered + * for this attribute, it should be called as well. */ updateValue: function( e ) { @@ -416,7 +418,30 @@ var editAttributeField = Backbone.View.extend( { } else { this.model.set( 'value', $el.val() ); } - }, + + var shortcodeName = this.shortcode.attributes.shortcode_tag, + attributeName = this.model.get( 'attr' ), + hookName = [ shortcodeName, attributeName ].join( '.' ), + changed = this.model.changed, + collection = _.flatten( _.values( this.views.parent.views._views ) ), + shortcode = this.shortcode; + + /* + * Action run when an attribute value changes on a shortcode + * + * Called as `{shortcodeName}.{attributeName}`. + * + * @param changed (object) + * The update, ie. { "changed": "newValue" } + * @param viewModels (array) + * The collections of views (editAttributeFields) + * which make up this shortcode UI form + * @param shortcode (object) + * Reference to the shortcode model which this attribute belongs to. + */ + wp.shortcake.hooks.doAction( hookName, changed, collection, shortcode ); + + } } ); diff --git a/js/build/field-color.js b/js/build/field-color.js index 5de8923d..bcc5db7c 100644 --- a/js/build/field-color.js +++ b/js/build/field-color.js @@ -87,7 +87,7 @@ var ShortcodeAttribute = Backbone.Model.extend({ description: '', meta: { placeholder: '', - } + }, }, }); @@ -209,9 +209,9 @@ module.exports = window.Shortcode_UI; },{"./../collections/shortcodes.js":2}],8:[function(require,module,exports){ (function (global){ -var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), -sui = require('./../utils/sui.js'), -$ = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), + sui = require('./../utils/sui.js'), + $ = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); var editAttributeField = Backbone.View.extend( { @@ -258,6 +258,7 @@ var editAttributeField = Backbone.View.extend( { data.meta = _meta.join( ' ' ); this.$el.html( this.template( data ) ); + this.updateValue(); return this }, @@ -266,7 +267,8 @@ var editAttributeField = Backbone.View.extend( { * Input Changed Update Callback. * * If the input field that has changed is for content or a valid attribute, - * then it should update the model. + * then it should update the model. If a callback function is registered + * for this attribute, it should be called as well. */ updateValue: function( e ) { @@ -283,7 +285,30 @@ var editAttributeField = Backbone.View.extend( { } else { this.model.set( 'value', $el.val() ); } - }, + + var shortcodeName = this.shortcode.attributes.shortcode_tag, + attributeName = this.model.get( 'attr' ), + hookName = [ shortcodeName, attributeName ].join( '.' ), + changed = this.model.changed, + collection = _.flatten( _.values( this.views.parent.views._views ) ), + shortcode = this.shortcode; + + /* + * Action run when an attribute value changes on a shortcode + * + * Called as `{shortcodeName}.{attributeName}`. + * + * @param changed (object) + * The update, ie. { "changed": "newValue" } + * @param viewModels (array) + * The collections of views (editAttributeFields) + * which make up this shortcode UI form + * @param shortcode (object) + * Reference to the shortcode model which this attribute belongs to. + */ + wp.shortcake.hooks.doAction( hookName, changed, collection, shortcode ); + + } } ); diff --git a/js/build/shortcode-ui.js b/js/build/shortcode-ui.js index cd9d2739..1457965c 100644 --- a/js/build/shortcode-ui.js +++ b/js/build/shortcode-ui.js @@ -120,7 +120,7 @@ var ShortcodeAttribute = Backbone.Model.extend({ description: '', meta: { placeholder: '', - } + }, }, }); @@ -586,9 +586,9 @@ module.exports = window.Shortcode_UI; },{"./../collections/shortcodes.js":2}],10:[function(require,module,exports){ (function (global){ -var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), -sui = require('./../utils/sui.js'), -$ = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), + sui = require('./../utils/sui.js'), + $ = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); var editAttributeField = Backbone.View.extend( { @@ -635,6 +635,7 @@ var editAttributeField = Backbone.View.extend( { data.meta = _meta.join( ' ' ); this.$el.html( this.template( data ) ); + this.updateValue(); return this }, @@ -643,7 +644,8 @@ var editAttributeField = Backbone.View.extend( { * Input Changed Update Callback. * * If the input field that has changed is for content or a valid attribute, - * then it should update the model. + * then it should update the model. If a callback function is registered + * for this attribute, it should be called as well. */ updateValue: function( e ) { @@ -660,7 +662,30 @@ var editAttributeField = Backbone.View.extend( { } else { this.model.set( 'value', $el.val() ); } - }, + + var shortcodeName = this.shortcode.attributes.shortcode_tag, + attributeName = this.model.get( 'attr' ), + hookName = [ shortcodeName, attributeName ].join( '.' ), + changed = this.model.changed, + collection = _.flatten( _.values( this.views.parent.views._views ) ), + shortcode = this.shortcode; + + /* + * Action run when an attribute value changes on a shortcode + * + * Called as `{shortcodeName}.{attributeName}`. + * + * @param changed (object) + * The update, ie. { "changed": "newValue" } + * @param viewModels (array) + * The collections of views (editAttributeFields) + * which make up this shortcode UI form + * @param shortcode (object) + * Reference to the shortcode model which this attribute belongs to. + */ + wp.shortcake.hooks.doAction( hookName, changed, collection, shortcode ); + + } } ); diff --git a/js/src/models/shortcode-attribute.js b/js/src/models/shortcode-attribute.js index 698ed4b3..5c302ded 100644 --- a/js/src/models/shortcode-attribute.js +++ b/js/src/models/shortcode-attribute.js @@ -9,7 +9,7 @@ var ShortcodeAttribute = Backbone.Model.extend({ description: '', meta: { placeholder: '', - } + }, }, }); diff --git a/js/src/views/edit-attribute-field.js b/js/src/views/edit-attribute-field.js index 5a18f792..a1fb86a4 100644 --- a/js/src/views/edit-attribute-field.js +++ b/js/src/views/edit-attribute-field.js @@ -1,6 +1,6 @@ -var Backbone = require('backbone'), -sui = require('sui-utils/sui'), -$ = require('jquery'); +var Backbone = require('backbone'), + sui = require('sui-utils/sui'), + $ = require('jquery'); var editAttributeField = Backbone.View.extend( { @@ -47,6 +47,7 @@ var editAttributeField = Backbone.View.extend( { data.meta = _meta.join( ' ' ); this.$el.html( this.template( data ) ); + this.updateValue(); return this }, @@ -55,7 +56,8 @@ var editAttributeField = Backbone.View.extend( { * Input Changed Update Callback. * * If the input field that has changed is for content or a valid attribute, - * then it should update the model. + * then it should update the model. If a callback function is registered + * for this attribute, it should be called as well. */ updateValue: function( e ) { @@ -72,7 +74,30 @@ var editAttributeField = Backbone.View.extend( { } else { this.model.set( 'value', $el.val() ); } - }, + + var shortcodeName = this.shortcode.attributes.shortcode_tag, + attributeName = this.model.get( 'attr' ), + hookName = [ shortcodeName, attributeName ].join( '.' ), + changed = this.model.changed, + collection = _.flatten( _.values( this.views.parent.views._views ) ), + shortcode = this.shortcode; + + /* + * Action run when an attribute value changes on a shortcode + * + * Called as `{shortcodeName}.{attributeName}`. + * + * @param changed (object) + * The update, ie. { "changed": "newValue" } + * @param viewModels (array) + * The collections of views (editAttributeFields) + * which make up this shortcode UI form + * @param shortcode (object) + * Reference to the shortcode model which this attribute belongs to. + */ + wp.shortcake.hooks.doAction( hookName, changed, collection, shortcode ); + + } } ); diff --git a/lib/wp-js-hooks/wp-js-hooks.js b/lib/wp-js-hooks/wp-js-hooks.js new file mode 100644 index 00000000..4c254389 --- /dev/null +++ b/lib/wp-js-hooks/wp-js-hooks.js @@ -0,0 +1,264 @@ +/** + * This code is taken from @carldanley's WP-JS-Hooks library: + * https://github.com/carldanley/WP-JS-Hooks + * + * This is a basic event manager based on the one proposed for WordPress core + * in https://core.trac.wordpress.org/attachment/ticket/21170. + * + * Modifications for this plugin: The EventManager methods are all namespaced + * to `wp.shortcake.hooks` to avoid collisions with the proposed system of + * hooks for core, which are intended to be adopted at `wp.hooks`. However, we + * plan to keep basic feature parity and interoperability with the proposed JS + * hooks and filters system for core, with the end goal of using the same API + * as what is finally decided on there. + */ + +( function( window, undefined ) { + 'use strict'; + + /** + * Handles managing all events for whatever you plug it into. Priorities for hooks are based on lowest to highest in + * that, lowest priority hooks are fired first. + */ + var EventManager = function() { + var slice = Array.prototype.slice; + + /** + * Maintain a reference to the object scope so our public methods never get confusing. + */ + var MethodsAvailable = { + removeFilter : removeFilter, + applyFilters : applyFilters, + addFilter : addFilter, + removeAction : removeAction, + doAction : doAction, + addAction : addAction + }; + + /** + * Contains the hooks that get registered with this EventManager. The array for storage utilizes a "flat" + * object literal such that looking up the hook utilizes the native object literal hash. + */ + var STORAGE = { + actions : {}, + filters : {} + }; + + /** + * Adds an action to the event manager. + * + * @param action Must contain namespace.identifier + * @param callback Must be a valid callback function before this action is added + * @param [priority=10] Used to control when the function is executed in relation to other callbacks bound to the same hook + * @param [context] Supply a value to be used for this + */ + function addAction( action, callback, priority, context ) { + if( typeof action === 'string' && typeof callback === 'function' ) { + priority = parseInt( ( priority || 10 ), 10 ); + _addHook( 'actions', action, callback, priority, context ); + } + + return MethodsAvailable; + } + + /** + * Performs an action if it exists. You can pass as many arguments as you want to this function; the only rule is + * that the first argument must always be the action. + */ + function doAction( /* action, arg1, arg2, ... */ ) { + var args = slice.call( arguments ); + var action = args.shift(); + + if( typeof action === 'string' ) { + _runHook( 'actions', action, args ); + } + + return MethodsAvailable; + } + + /** + * Removes the specified action if it contains a namespace.identifier & exists. + * + * @param action The action to remove + * @param [callback] Callback function to remove + */ + function removeAction( action, callback ) { + if( typeof action === 'string' ) { + _removeHook( 'actions', action, callback ); + } + + return MethodsAvailable; + } + + /** + * Adds a filter to the event manager. + * + * @param filter Must contain namespace.identifier + * @param callback Must be a valid callback function before this action is added + * @param [priority=10] Used to control when the function is executed in relation to other callbacks bound to the same hook + * @param [context] Supply a value to be used for this + */ + function addFilter( filter, callback, priority, context ) { + if( typeof filter === 'string' && typeof callback === 'function' ) { + priority = parseInt( ( priority || 10 ), 10 ); + _addHook( 'filters', filter, callback, priority, context ); + } + + return MethodsAvailable; + } + + /** + * Performs a filter if it exists. You should only ever pass 1 argument to be filtered. The only rule is that + * the first argument must always be the filter. + */ + function applyFilters( /* filter, filtered arg, arg2, ... */ ) { + var args = slice.call( arguments ); + var filter = args.shift(); + + if( typeof filter === 'string' ) { + return _runHook( 'filters', filter, args ); + } + + return MethodsAvailable; + } + + /** + * Removes the specified filter if it contains a namespace.identifier & exists. + * + * @param filter The action to remove + * @param [callback] Callback function to remove + */ + function removeFilter( filter, callback ) { + if( typeof filter === 'string') { + _removeHook( 'filters', filter, callback ); + } + + return MethodsAvailable; + } + + /** + * Removes the specified hook by resetting the value of it. + * + * @param type Type of hook, either 'actions' or 'filters' + * @param hook The hook (namespace.identifier) to remove + * @private + */ + function _removeHook( type, hook, callback, context ) { + var handlers, handler, i; + + if ( !STORAGE[ type ][ hook ] ) { + return; + } + if ( !callback ) { + STORAGE[ type ][ hook ] = []; + } else { + handlers = STORAGE[ type ][ hook ]; + if ( !context ) { + for ( i = handlers.length; i--; ) { + if ( handlers[i].callback === callback ) { + handlers.splice( i, 1 ); + } + } + } + else { + for ( i = handlers.length; i--; ) { + handler = handlers[i]; + if ( handler.callback === callback && handler.context === context) { + handlers.splice( i, 1 ); + } + } + } + } + } + + /** + * Adds the hook to the appropriate storage container + * + * @param type 'actions' or 'filters' + * @param hook The hook (namespace.identifier) to add to our event manager + * @param callback The function that will be called when the hook is executed. + * @param priority The priority of this hook. Must be an integer. + * @param [context] A value to be used for this + * @private + */ + function _addHook( type, hook, callback, priority, context ) { + var hookObject = { + callback : callback, + priority : priority, + context : context + }; + + // Utilize 'prop itself' : http://jsperf.com/hasownproperty-vs-in-vs-undefined/19 + var hooks = STORAGE[ type ][ hook ]; + if( hooks ) { + hooks.push( hookObject ); + hooks = _hookInsertSort( hooks ); + } + else { + hooks = [ hookObject ]; + } + + STORAGE[ type ][ hook ] = hooks; + } + + /** + * Use an insert sort for keeping our hooks organized based on priority. This function is ridiculously faster + * than bubble sort, etc: http://jsperf.com/javascript-sort + * + * @param hooks The custom array containing all of the appropriate hooks to perform an insert sort on. + * @private + */ + function _hookInsertSort( hooks ) { + var tmpHook, j, prevHook; + for( var i = 1, len = hooks.length; i < len; i++ ) { + tmpHook = hooks[ i ]; + j = i; + while( ( prevHook = hooks[ j - 1 ] ) && prevHook.priority > tmpHook.priority ) { + hooks[ j ] = hooks[ j - 1 ]; + --j; + } + hooks[ j ] = tmpHook; + } + + return hooks; + } + + /** + * Runs the specified hook. If it is an action, the value is not modified but if it is a filter, it is. + * + * @param type 'actions' or 'filters' + * @param hook The hook ( namespace.identifier ) to be ran. + * @param args Arguments to pass to the action/filter. If it's a filter, args is actually a single parameter. + * @private + */ + function _runHook( type, hook, args ) { + var handlers = STORAGE[ type ][ hook ], i, len; + + if ( !handlers ) { + return (type === 'filters') ? args[0] : false; + } + + len = handlers.length; + if ( type === 'filters' ) { + for ( i = 0; i < len; i++ ) { + args[ 0 ] = handlers[ i ].callback.apply( handlers[ i ].context, args ); + } + } else { + for ( i = 0; i < len; i++ ) { + handlers[ i ].callback.apply( handlers[ i ].context, args ); + } + } + + return ( type === 'filters' ) ? args[ 0 ] : true; + } + + // return all of the publicly available methods + return MethodsAvailable; + + }; + + window.wp = window.wp || {}; + window.wp.shortcake = window.wp.shortcake || {}; + window.wp.shortcake.hooks = new EventManager(); + +} )( window ); diff --git a/lib/wp-js-hooks/wp-js-hooks.min.js b/lib/wp-js-hooks/wp-js-hooks.min.js new file mode 100644 index 00000000..15729792 --- /dev/null +++ b/lib/wp-js-hooks/wp-js-hooks.min.js @@ -0,0 +1 @@ +!function(t){"use strict";var r=function(){function t(t,r,n,e){return"string"==typeof t&&"function"==typeof r&&(n=parseInt(n||10,10),f("actions",t,r,n,e)),p}function r(){var t=s.call(arguments),r=t.shift();return"string"==typeof r&&l("actions",r,t),p}function n(t,r){return"string"==typeof t&&c("actions",t,r),p}function e(t,r,n,e){return"string"==typeof t&&"function"==typeof r&&(n=parseInt(n||10,10),f("filters",t,r,n,e)),p}function i(){var t=s.call(arguments),r=t.shift();return"string"==typeof r?l("filters",r,t):p}function o(t,r){return"string"==typeof t&&c("filters",t,r),p}function c(t,r,n,e){var i,o,c;if(u[t][r])if(n)if(i=u[t][r],e)for(c=i.length;c--;)o=i[c],o.callback===n&&o.context===e&&i.splice(c,1);else for(c=i.length;c--;)i[c].callback===n&&i.splice(c,1);else u[t][r]=[]}function f(t,r,n,e,i){var o={callback:n,priority:e,context:i},c=u[t][r];c?(c.push(o),c=a(c)):c=[o],u[t][r]=c}function a(t){for(var r,n,e,i=1,o=t.length;o>i;i++){for(r=t[i],n=i;(e=t[n-1])&&e.priority>r.priority;)t[n]=t[n-1],--n;t[n]=r}return t}function l(t,r,n){var e,i,o=u[t][r];if(!o)return"filters"===t?n[0]:!1;if(i=o.length,"filters"===t)for(e=0;i>e;e++)n[0]=o[e].callback.apply(o[e].context,n);else for(e=0;i>e;e++)o[e].callback.apply(o[e].context,n);return"filters"===t?n[0]:!0}var s=Array.prototype.slice,p={removeFilter:o,applyFilters:i,addFilter:e,removeAction:n,doAction:r,addAction:t},u={actions:{},filters:{}};return p};t.wp=t.wp||{},t.wp.shortcake=t.wp.shortcake||{},t.wp.shortcake.hooks=new r}(window); \ No newline at end of file