diff --git a/dev.php b/dev.php index 5a256ba7..a08d9632 100644 --- a/dev.php +++ b/dev.php @@ -61,64 +61,68 @@ function shortcode_ui_dev_advanced_example() { */ shortcode_ui_register_for_shortcode( 'shortcake_dev', array( - 'label' => 'Shortcake Dev', // Display label. String. Required. - 'listItemImage' => 'dashicons-editor-quote', // Icon/attachment for shortcode. Optional. src or dashicons-$icon. Defaults to carrot. - 'inner_content' => array( - 'label' => 'Quote', - ), - 'post_type' => array( 'post' ), //Post type support - // Available shortcode attributes and default values. Required. Array. - // Attribute model expects 'attr', 'type' and 'label' - // Supported field types: text, checkbox, textarea, radio, select, email, url, number, and date. - 'attrs' => array( - array( - 'label' => __( 'Attachment', 'your-text-domain' ), // Field label - 'attr' => 'attachment', // Field type - 'type' => 'attachment', - 'libraryType' => array( 'image' ), // Media type to insert - 'addButton' => __( 'Select Image', 'your-text-domain' ), // Button text that opens Media Library - 'frameTitle' => __( 'Select Image', 'your-text-domain ' ), // Media Library frame title - ), - array( - 'label' => __( 'Citation Source', 'your-text-domain' ), - 'attr' => 'source', - 'type' => 'text', - 'meta' => array( // Holds custom field attributes. - 'placeholder' => 'Test placeholder', - 'data-test' => 1, // Custom data attribute - ), - ), - array( - 'label' => 'Select Page', - 'attr' => 'page', - 'type' => 'post_select', - 'query' => array( 'post_type' => 'page' ), - 'multiple' => true, - ), - ), + 'label' => 'Shortcake Dev', // Display label. String. Required. + 'listItemImage' => 'dashicons-editor-quote', // Icon/attachment for shortcode. Optional. src or dashicons-$icon. Defaults to carrot. + 'inner_content' => array( + 'label' => 'Quote', + ), + 'post_type' => array( 'post' ), //Post type support + // Available shortcode attributes and default values. Required. Array. + // Attribute model expects 'attr', 'type' and 'label' + // Supported field types: text, checkbox, textarea, radio, select, email, url, number, and date. + 'attrs' => array( + array( + 'label' => esc_html__( 'Attachment', 'your-text-domain' ), // Field label + 'attr' => 'attachment', // Field type + 'type' => 'attachment', + 'libraryType' => array( 'image' ), // Media type to insert + 'addButton' => esc_html__( 'Select Image', 'your-text-domain' ), // Button text that opens Media Library + 'frameTitle' => esc_html__( 'Select Image', 'your-text-domain ' ), // Media Library frame title + ), + array( + 'label' => esc_html__( 'Citation Source', 'your-text-domain' ), + 'attr' => 'source', + 'type' => 'text', + 'encode' => true, + 'meta' => array( // Holds custom field attributes. + 'placeholder' => 'Test placeholder', + 'data-test' => 1, // Custom data attribute + ), + ), + array( + 'label' => 'Select Page', + 'attr' => 'page', + 'type' => 'post_select', + 'query' => array( 'post_type' => 'page' ), + 'multiple' => true, + ), + ), ) ); } -function shortcode_ui_dev_shortcode( $attr, $content = '' ) { +function shortcode_ui_dev_shortcode( $attr, $content = '', $shortcode_tag ) { - //Parse the attribute of the shortcode - $attr = wp_parse_args( $attr, array( - 'source' => '', - 'attachment' => 0 - ) ); + $attr = shortcode_atts( array( + 'source' => '', + 'attachment' => 0, + 'source' => null, + ), $attr, $shortcode_tag ); ob_start(); + ?>
-

+

Content:
- Source:
+ Source:
Image:
-

+

shortcodes[ $shortcode_tag ] = $args; + // Setup filter to handle decoding encoded attributes. + add_filter( "shortcode_atts_{$shortcode_tag}", array( $this, 'filter_shortcode_atts_decode_encoded' ), 5, 3 ); + } /** @@ -384,4 +387,40 @@ public function handle_ajax_bulk_do_shortcode() { } } + + /** + * Decode any encoded attributes. + * + * @param array $out The output array of shortcode attributes. + * @param array $pairs The supported attributes and their defaults. + * @param array $atts The user defined shortcode attributes. + * @return array $out The output array of shortcode attributes. + */ + public function filter_shortcode_atts_decode_encoded( $out, $pairs, $atts ) { + + // Get current shortcode tag from the current filter + // by stripping `shortcode_atts_` from start of string. + $shortcode_tag = substr( current_filter(), 15 ); + + if ( ! isset( $this->shortcodes[ $shortcode_tag ] ) ) { + return $out; + } + + $fields = Shortcode_UI_Fields::get_instance()->get_fields(); + $args = $this->shortcodes[ $shortcode_tag ]; + + foreach ( $args['attrs'] as $attr ) { + + $default = isset( $fields[ $attr['type'] ]['encode'] ) ? $fields[ $attr['type'] ]['encode'] : false; + $encoded = isset( $attr['encode'] ) ? $attr['encode'] : $default; + + if ( $encoded && isset( $out[ $attr['attr'] ] ) ) { + $out[ $attr['attr'] ] = rawurldecode( $out[ $attr['attr'] ] ); + } + } + + return $out; + + } + } diff --git a/inc/fields/class-shortcode-ui-fields.php b/inc/fields/class-shortcode-ui-fields.php index f4a426ec..08b2f8db 100644 --- a/inc/fields/class-shortcode-ui-fields.php +++ b/inc/fields/class-shortcode-ui-fields.php @@ -22,6 +22,7 @@ class Shortcode_UI_Fields { private $field_defaults = array( 'template' => 'shortcode-ui-field-text', 'view' => 'editAttributeField', + 'encode' => false, ); /** diff --git a/js-tests/build/specs.js b/js-tests/build/specs.js index 6f373cc6..9a21e777 100644 --- a/js-tests/build/specs.js +++ b/js-tests/build/specs.js @@ -38,6 +38,7 @@ describe( "Shortcode Attribute Model", function() { type: 'text', value: 'test value', description: 'test description', + encode: false, meta: { placeholder: 'test placeholder' }, @@ -73,7 +74,7 @@ describe( "Shortcode Model", function() { label: 'Attribute', type: 'text', value: 'test value', - placeholder: 'test placeholder', + placeholder: 'test placeholder' } ], inner_content: { @@ -126,8 +127,30 @@ describe( "Shortcode Model", function() { }); -}); + it( 'Format shortcode with encoded attributes.', function() { + + var shortcode_encoded_attribute, formatted, expected; + + shortcode_encoded_attribute = new Shortcode({ + label: 'Test Label', + shortcode_tag: 'test_shortcode_encoded', + attrs: [ + { + attr: 'attr', + type: 'text', + value: 'bar', + encode: true, + }, + ], + }); + + formatted = shortcode_encoded_attribute.formatShortcode(); + expected = '[test_shortcode_encoded attr="%3Cb%20class%3D%22foo%22%3Ebar%3C%2Fb%3E"]'; + expect( formatted ).toEqual( expected ); + }); + +}); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"../../js/src/collections/shortcode-attributes":7,"../../js/src/models/inner-content":9,"../../js/src/models/shortcode":11,"../../js/src/models/shortcode-attribute":10}],4:[function(require,module,exports){ @@ -224,6 +247,18 @@ describe( "MCE View Constructor", function() { }, } ) ); + sui.shortcodes.push( new Shortcode( { + label: 'Test Label', + shortcode_tag: 'test_shortcode_encoded', + attrs: [ + { + attr: 'attr', + label: 'Attribute', + encode: true, + } + ], + } ) ); + it ( 'test get shortcode model', function() { var constructor = jQuery.extend( true, {}, MceViewConstructor ); @@ -349,6 +384,12 @@ describe( "MCE View Constructor", function() { expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).toEqual( 'test' ); }); + it( 'parses shortcode with encoded attribute', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test_shortcode_encoded attr="%3Cb%20class%3D%22foo%22%3Ebar%3C%2Fb%3E"]'); + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere({ attr: 'attr' }).get('value') ).toEqual( 'bar' ); + }); + } ); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) @@ -428,16 +469,19 @@ module.exports = InnerContent; var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); var ShortcodeAttribute = Backbone.Model.extend({ + defaults: { attr: '', label: '', type: '', value: '', description: '', + encode: false, meta: { placeholder: '', }, }, + }); module.exports = ShortcodeAttribute; @@ -520,6 +564,11 @@ Shortcode = Backbone.Model.extend({ return; } + // Encode textareas incase HTML + if ( attr.get( 'encode' ) ) { + attr.set( 'value', encodeURIComponent( decodeURIComponent( attr.get( 'value' ) ) ), { silent: true } ); + } + attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); } ); @@ -684,16 +733,23 @@ var shortcodeViewConstructor = { shortcodeModel = shortcodeModel.clone(); - shortcodeModel.get('attrs').each( - function( attr ) { - if ( attr.get('attr') in options.attrs.named ) { - attr.set( - 'value', - options.attrs.named[ attr.get('attr') ] - ); - } + shortcodeModel.get('attrs').each( function( attr ) { + + // Verify value exists for attribute. + if ( ! ( attr.get('attr') in options.attrs.named ) ) { + return; + } + + var value = options.attrs.named[ attr.get('attr') ]; + + // Maybe decode value. + if ( attr.get('encode') ) { + value = decodeURIComponent( value ); } - ); + + attr.set( 'value', value ); + + } ); if ( 'content' in options ) { var innerContent = shortcodeModel.get('inner_content'); @@ -815,20 +871,27 @@ var shortcodeViewConstructor = { var attributes_backup = {}; var attributes = wp.shortcode.attrs( matches[3] ); + for ( var key in attributes.named ) { + if ( ! attributes.named.hasOwnProperty( key ) ) { continue; } + value = attributes.named[ key ]; - attr = currentShortcode.get( 'attrs' ).findWhere({ - attr : key - }); + attr = currentShortcode.get( 'attrs' ).findWhere({ attr: key }); + + if ( attr && attr.get('encode') ) { + value = decodeURIComponent( value ); + } + if ( attr ) { attr.set( 'value', value ); } else { attributes_backup[ key ] = value; } } + currentShortcode.set( 'attributes_backup', attributes_backup ); if ( matches[5] ) { diff --git a/js-tests/src/shortcodeAttributeModelSpec.js b/js-tests/src/shortcodeAttributeModelSpec.js index b0a2d744..e3d87f23 100644 --- a/js-tests/src/shortcodeAttributeModelSpec.js +++ b/js-tests/src/shortcodeAttributeModelSpec.js @@ -8,6 +8,7 @@ describe( "Shortcode Attribute Model", function() { type: 'text', value: 'test value', description: 'test description', + encode: false, meta: { placeholder: 'test placeholder' }, diff --git a/js-tests/src/shortcodeModelSpec.js b/js-tests/src/shortcodeModelSpec.js index e7fbe0af..341cf2a7 100644 --- a/js-tests/src/shortcodeModelSpec.js +++ b/js-tests/src/shortcodeModelSpec.js @@ -17,7 +17,7 @@ describe( "Shortcode Model", function() { label: 'Attribute', type: 'text', value: 'test value', - placeholder: 'test placeholder', + placeholder: 'test placeholder' } ], inner_content: { @@ -70,5 +70,27 @@ describe( "Shortcode Model", function() { }); -}); + it( 'Format shortcode with encoded attributes.', function() { + + var shortcode_encoded_attribute, formatted, expected; + + shortcode_encoded_attribute = new Shortcode({ + label: 'Test Label', + shortcode_tag: 'test_shortcode_encoded', + attrs: [ + { + attr: 'attr', + type: 'text', + value: 'bar', + encode: true, + }, + ], + }); + + formatted = shortcode_encoded_attribute.formatShortcode(); + expected = '[test_shortcode_encoded attr="%3Cb%20class%3D%22foo%22%3Ebar%3C%2Fb%3E"]'; + expect( formatted ).toEqual( expected ); + }); + +}); diff --git a/js-tests/src/utils/mceViewConstructorSpec.js b/js-tests/src/utils/mceViewConstructorSpec.js index 40638d26..befb43d3 100644 --- a/js-tests/src/utils/mceViewConstructorSpec.js +++ b/js-tests/src/utils/mceViewConstructorSpec.js @@ -40,6 +40,18 @@ describe( "MCE View Constructor", function() { }, } ) ); + sui.shortcodes.push( new Shortcode( { + label: 'Test Label', + shortcode_tag: 'test_shortcode_encoded', + attrs: [ + { + attr: 'attr', + label: 'Attribute', + encode: true, + } + ], + } ) ); + it ( 'test get shortcode model', function() { var constructor = jQuery.extend( true, {}, MceViewConstructor ); @@ -165,4 +177,10 @@ describe( "MCE View Constructor", function() { expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).toEqual( 'test' ); }); + it( 'parses shortcode with encoded attribute', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test_shortcode_encoded attr="%3Cb%20class%3D%22foo%22%3Ebar%3C%2Fb%3E"]'); + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere({ attr: 'attr' }).get('value') ).toEqual( 'bar' ); + }); + } ); diff --git a/js/build/field-attachment.js b/js/build/field-attachment.js new file mode 100644 index 00000000..87ac2206 --- /dev/null +++ b/js/build/field-attachment.js @@ -0,0 +1,438 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o'); + + if ( 'image' !== attachment.type ) { + + $( '', { + src: attachment.icon, + alt: attachment.title, + } ).appendTo( $thumbnail ); + + $( '
', { + class: 'filename', + html: '
' + attachment.title + '
', + } ).appendTo( $thumbnail ); + + } else { + + $( '', { + src: attachment.sizes.thumbnail.url, + width: attachment.sizes.thumbnail.width, + height: attachment.sizes.thumbnail.height, + alt: attachment.alt, + } ) .appendTo( $thumbnail ) + + } + + $thumbnail.find( 'img' ).wrap( '
' ); + $container.append( $thumbnail ); + $container.toggleClass( 'has-attachment', true ); + + } + + /** + * Remove the attachment. + * Render preview & Update the model. + */ + var removeAttachment = function() { + + model.set( 'value', null ); + + $container.toggleClass( 'has-attachment', false ); + $container.toggleClass( 'has-attachment', false ); + $container.find( '.thumbnail' ).remove(); + } + + // Add initial Attachment if available. + updateAttachment( model.get( 'value' ) ); + + // Remove file when the button is clicked. + $removeButton.click( function(e) { + e.preventDefault(); + removeAttachment(); + }); + + // Open media frame when add button is clicked + $addButton.click( function(e) { + e.preventDefault(); + frame.open(); + } ); + + // Update the attachment when an item is selected. + frame.on( 'select', function() { + + var selection = frame.state().get('selection'); + attachment = selection.first(); + + updateAttachment( attachment.id ); + + frame.close(); + + }); + + } + +} ); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./utils/sui.js":7,"./views/edit-attribute-field.js":8}],4:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); + +/** + * Shortcode Attribute Model. + */ +var InnerContent = Backbone.Model.extend({ + defaults : { + label: shortcodeUIData.strings.insert_content_label, + type: 'textarea', + value: '', + placeholder: '', + }, +}); + +module.exports = InnerContent; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],5:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); + +var ShortcodeAttribute = Backbone.Model.extend({ + defaults: { + attr: '', + label: '', + type: '', + value: '', + description: '', + escape: false, + meta: { + placeholder: '', + } + }, +}); + +module.exports = ShortcodeAttribute; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],6:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); +var ShortcodeAttributes = require('./../collections/shortcode-attributes.js'); +var InnerContent = require('./inner-content.js'); + +Shortcode = Backbone.Model.extend({ + + defaults: { + label: '', + shortcode_tag: '', + attrs: new ShortcodeAttributes, + }, + + /** + * Custom set method. + * Handles setting the attribute collection. + */ + set: function( attributes, options ) { + + if ( attributes.attrs !== undefined && ! ( attributes.attrs instanceof ShortcodeAttributes ) ) { + attributes.attrs = new ShortcodeAttributes( attributes.attrs ); + } + + if ( attributes.inner_content && ! ( attributes.inner_content instanceof InnerContent ) ) { + attributes.inner_content = new InnerContent( attributes.inner_content ); + } + + return Backbone.Model.prototype.set.call(this, attributes, options); + }, + + /** + * Custom toJSON. + * Handles converting the attribute collection to JSON. + */ + toJSON: function( options ) { + options = Backbone.Model.prototype.toJSON.call(this, options); + if ( options.attrs && ( options.attrs instanceof ShortcodeAttributes ) ) { + options.attrs = options.attrs.toJSON(); + } + if ( options.inner_content && ( options.inner_content instanceof InnerContent ) ) { + options.inner_content = options.inner_content.toJSON(); + } + return options; + }, + + /** + * Custom clone + * Make sure we don't clone a reference to attributes. + */ + clone: function() { + var clone = Backbone.Model.prototype.clone.call( this ); + clone.set( 'attrs', clone.get( 'attrs' ).clone() ); + if ( clone.get( 'inner_content' ) ) { + clone.set( 'inner_content', clone.get( 'inner_content' ).clone() ); + } + return clone; + }, + + /** + * Get the shortcode as... a shortcode! + * + * @return string eg [shortcode attr1=value] + */ + formatShortcode: function() { + + var template, shortcodeAttributes, attrs = [], content, self = this; + + this.get( 'attrs' ).each( function( attr ) { + + // Skip empty attributes. + if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { + return; + } + + var type = attr.get( 'type' ); + + // Encode textareas incase HTML + if ( shortcodeUIFieldData[ type ] && shortcodeUIFieldData[ type ].escape ) { + attr.set( 'value', encodeURIComponent( decodeURIComponent( attr.get( 'value' ) ) ) ); + } + + attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); + + } ); + + if ( this.get( 'inner_content' ) ) { + content = this.get( 'inner_content' ).get( 'value' ); + } + + template = "[{{ shortcode }} {{ attributes }}]" + + if ( content && content.length > 0 ) { + template += "{{ content }}[/{{ shortcode }}]" + } + + template = template.replace( /{{ shortcode }}/g, this.get('shortcode_tag') ); + template = template.replace( /{{ attributes }}/g, attrs.join( ' ' ) ); + template = template.replace( /{{ content }}/g, content ); + + return template; + + } + +}); + +module.exports = Shortcode; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../collections/shortcode-attributes.js":1,"./inner-content.js":4}],7:[function(require,module,exports){ +var Shortcodes = require('./../collections/shortcodes.js'); + +window.Shortcode_UI = window.Shortcode_UI || { + shortcodes: new Shortcodes, + views: {}, + controllers: {}, +}; + +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 editAttributeField = Backbone.View.extend( { + + tagName: "div", + + events: { + 'keyup input[type="text"]': 'updateValue', + 'keyup textarea': 'updateValue', + 'change select': 'updateValue', + 'change input[type=checkbox]': 'updateValue', + 'change input[type=radio]': 'updateValue', + 'change input[type=email]': 'updateValue', + 'change input[type=number]': 'updateValue', + 'change input[type=date]': 'updateValue', + 'change input[type=url]': 'updateValue', + }, + + render: function() { + + var data = jQuery.extend( { + id: 'shortcode-ui-' + this.model.get( 'attr' ) + '-' + this.model.cid, + }, this.model.toJSON() ); + + // Handle legacy custom meta. + // Can be removed in 0.4. + if ( data.placeholder ) { + data.meta.placeholder = data.placeholder; + delete data.placeholder; + } + + // Convert meta JSON to attribute string. + var _meta = []; + for ( var key in data.meta ) { + + // Boolean attributes can only require attribute key, not value. + if ( 'boolean' === typeof( data.meta[ key ] ) ) { + + // Only set truthy boolean attributes. + if ( data.meta[ key ] ) { + _meta.push( _.escape( key ) ); + } + + } else { + + _meta.push( _.escape( key ) + '="' + _.escape( data.meta[ key ] ) + '"' ); + + } + + } + + data.meta = _meta.join( ' ' ); + + this.$el.html( this.template( data ) ); + + return this + }, + + /** + * Input Changed Update Callback. + * + * If the input field that has changed is for content or a valid attribute, + * then it should update the model. + */ + updateValue: function( e ) { + + if ( this.model.get( 'attr' ) ) { + var $el = $( this.el ).find( '[name=' + this.model.get( 'attr' ) + ']' ); + } else { + var $el = $( this.el ).find( '[name="inner_content"]' ); + } + + if ( 'radio' === this.model.attributes.type ) { + this.model.set( 'value', $el.filter(':checked').first().val() ); + } else if ( 'checkbox' === this.model.attributes.type ) { + this.model.set( 'value', $el.is( ':checked' ) ); + } else { + this.model.set( 'value', $el.val() ); + } + }, + +} ); + +sui.views.editAttributeField = editAttributeField; +module.exports = editAttributeField; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../utils/sui.js":7}]},{},[3]); diff --git a/js/build/field-color.js b/js/build/field-color.js new file mode 100644 index 00000000..ba92bf0e --- /dev/null +++ b/js/build/field-color.js @@ -0,0 +1,309 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ) { + template += "{{ content }}[/{{ shortcode }}]" + } + + template = template.replace( /{{ shortcode }}/g, this.get('shortcode_tag') ); + template = template.replace( /{{ attributes }}/g, attrs.join( ' ' ) ); + template = template.replace( /{{ content }}/g, content ); + + return template; + + } + +}); + +module.exports = Shortcode; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../collections/shortcode-attributes.js":1,"./inner-content.js":4}],7:[function(require,module,exports){ +var Shortcodes = require('./../collections/shortcodes.js'); + +window.Shortcode_UI = window.Shortcode_UI || { + shortcodes: new Shortcodes, + views: {}, + controllers: {}, +}; + +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 editAttributeField = Backbone.View.extend( { + + tagName: "div", + + events: { + 'keyup input[type="text"]': 'updateValue', + 'keyup textarea': 'updateValue', + 'change select': 'updateValue', + 'change input[type=checkbox]': 'updateValue', + 'change input[type=radio]': 'updateValue', + 'change input[type=email]': 'updateValue', + 'change input[type=number]': 'updateValue', + 'change input[type=date]': 'updateValue', + 'change input[type=url]': 'updateValue', + }, + + render: function() { + + var data = jQuery.extend( { + id: 'shortcode-ui-' + this.model.get( 'attr' ) + '-' + this.model.cid, + }, this.model.toJSON() ); + + // Handle legacy custom meta. + // Can be removed in 0.4. + if ( data.placeholder ) { + data.meta.placeholder = data.placeholder; + delete data.placeholder; + } + + // Convert meta JSON to attribute string. + var _meta = []; + for ( var key in data.meta ) { + + // Boolean attributes can only require attribute key, not value. + if ( 'boolean' === typeof( data.meta[ key ] ) ) { + + // Only set truthy boolean attributes. + if ( data.meta[ key ] ) { + _meta.push( _.escape( key ) ); + } + + } else { + + _meta.push( _.escape( key ) + '="' + _.escape( data.meta[ key ] ) + '"' ); + + } + + } + + data.meta = _meta.join( ' ' ); + + this.$el.html( this.template( data ) ); + + return this + }, + + /** + * Input Changed Update Callback. + * + * If the input field that has changed is for content or a valid attribute, + * then it should update the model. + */ + updateValue: function( e ) { + + if ( this.model.get( 'attr' ) ) { + var $el = $( this.el ).find( '[name=' + this.model.get( 'attr' ) + ']' ); + } else { + var $el = $( this.el ).find( '[name="inner_content"]' ); + } + + if ( 'radio' === this.model.attributes.type ) { + this.model.set( 'value', $el.filter(':checked').first().val() ); + } else if ( 'checkbox' === this.model.attributes.type ) { + this.model.set( 'value', $el.is( ':checked' ) ); + } else { + this.model.set( 'value', $el.val() ); + } + }, + +} ); + +sui.views.editAttributeField = editAttributeField; +module.exports = editAttributeField; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../utils/sui.js":7}]},{},[3]); diff --git a/js/build/shortcode-ui.js b/js/build/shortcode-ui.js index 957b1d58..9593582e 100644 --- a/js/build/shortcode-ui.js +++ b/js/build/shortcode-ui.js @@ -113,16 +113,19 @@ module.exports = InnerContent; var Backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null); var ShortcodeAttribute = Backbone.Model.extend({ + defaults: { attr: '', label: '', type: '', value: '', description: '', + encode: false, meta: { placeholder: '', }, }, + }); module.exports = ShortcodeAttribute; @@ -205,6 +208,11 @@ Shortcode = Backbone.Model.extend({ return; } + // Encode textareas incase HTML + if ( attr.get( 'encode' ) ) { + attr.set( 'value', encodeURIComponent( decodeURIComponent( attr.get( 'value' ) ) ), { silent: true } ); + } + attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); } ); @@ -400,16 +408,23 @@ var shortcodeViewConstructor = { shortcodeModel = shortcodeModel.clone(); - shortcodeModel.get('attrs').each( - function( attr ) { - if ( attr.get('attr') in options.attrs.named ) { - attr.set( - 'value', - options.attrs.named[ attr.get('attr') ] - ); - } + shortcodeModel.get('attrs').each( function( attr ) { + + // Verify value exists for attribute. + if ( ! ( attr.get('attr') in options.attrs.named ) ) { + return; } - ); + + var value = options.attrs.named[ attr.get('attr') ]; + + // Maybe decode value. + if ( attr.get('encode') ) { + value = decodeURIComponent( value ); + } + + attr.set( 'value', value ); + + } ); if ( 'content' in options ) { var innerContent = shortcodeModel.get('inner_content'); @@ -531,20 +546,27 @@ var shortcodeViewConstructor = { var attributes_backup = {}; var attributes = wp.shortcode.attrs( matches[3] ); + for ( var key in attributes.named ) { + if ( ! attributes.named.hasOwnProperty( key ) ) { continue; } + value = attributes.named[ key ]; - attr = currentShortcode.get( 'attrs' ).findWhere({ - attr : key - }); + attr = currentShortcode.get( 'attrs' ).findWhere({ attr: key }); + + if ( attr && attr.get('encode') ) { + value = decodeURIComponent( value ); + } + if ( attr ) { attr.set( 'value', value ); } else { attributes_backup[ key ] = value; } } + currentShortcode.set( 'attributes_backup', attributes_backup ); if ( matches[5] ) { @@ -1338,8 +1360,8 @@ module.exports = editAttributeField; },{"./../utils/sui.js":10}],15:[function(require,module,exports){ (function (global){ var wp = (typeof window !== "undefined" ? window['wp'] : typeof global !== "undefined" ? global['wp'] : null), - sui = require('./../utils/sui.js'), - backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null), +sui = require('./../utils/sui.js'), +backbone = (typeof window !== "undefined" ? window['Backbone'] : typeof global !== "undefined" ? global['Backbone'] : null), editAttributeField = require('./edit-attribute-field.js'), // Additional attribute field types: these fields are all standalone in functionality, @@ -1366,7 +1388,7 @@ var EditShortcodeForm = wp.Backbone.View.extend({ var view = new editAttributeField( { model: innerContent } ); view.shortcode = t.model; - view.template = wp.media.template( 'shortcode-ui-content' ); + view.template = wp.media.template( 'shortcode-ui-content' ); t.views.add( '.edit-shortcode-form-fields', view ); diff --git a/js/src/models/shortcode-attribute.js b/js/src/models/shortcode-attribute.js index 5c302ded..d88a7d72 100644 --- a/js/src/models/shortcode-attribute.js +++ b/js/src/models/shortcode-attribute.js @@ -1,16 +1,19 @@ var Backbone = require('backbone'); var ShortcodeAttribute = Backbone.Model.extend({ + defaults: { attr: '', label: '', type: '', value: '', description: '', + encode: false, meta: { placeholder: '', }, }, + }); module.exports = ShortcodeAttribute; diff --git a/js/src/models/shortcode.js b/js/src/models/shortcode.js index 74da3804..af53bffb 100644 --- a/js/src/models/shortcode.js +++ b/js/src/models/shortcode.js @@ -73,6 +73,11 @@ Shortcode = Backbone.Model.extend({ return; } + // Encode textareas incase HTML + if ( attr.get( 'encode' ) ) { + attr.set( 'value', encodeURIComponent( decodeURIComponent( attr.get( 'value' ) ) ), { silent: true } ); + } + attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); } ); diff --git a/js/src/utils/shortcode-view-constructor.js b/js/src/utils/shortcode-view-constructor.js index 0bfc8580..22ef4830 100644 --- a/js/src/utils/shortcode-view-constructor.js +++ b/js/src/utils/shortcode-view-constructor.js @@ -46,16 +46,23 @@ var shortcodeViewConstructor = { shortcodeModel = shortcodeModel.clone(); - shortcodeModel.get('attrs').each( - function( attr ) { - if ( attr.get('attr') in options.attrs.named ) { - attr.set( - 'value', - options.attrs.named[ attr.get('attr') ] - ); - } + shortcodeModel.get('attrs').each( function( attr ) { + + // Verify value exists for attribute. + if ( ! ( attr.get('attr') in options.attrs.named ) ) { + return; } - ); + + var value = options.attrs.named[ attr.get('attr') ]; + + // Maybe decode value. + if ( attr.get('encode') ) { + value = decodeURIComponent( value ); + } + + attr.set( 'value', value ); + + } ); if ( 'content' in options ) { var innerContent = shortcodeModel.get('inner_content'); @@ -177,20 +184,27 @@ var shortcodeViewConstructor = { var attributes_backup = {}; var attributes = wp.shortcode.attrs( matches[3] ); + for ( var key in attributes.named ) { + if ( ! attributes.named.hasOwnProperty( key ) ) { continue; } + value = attributes.named[ key ]; - attr = currentShortcode.get( 'attrs' ).findWhere({ - attr : key - }); + attr = currentShortcode.get( 'attrs' ).findWhere({ attr: key }); + + if ( attr && attr.get('encode') ) { + value = decodeURIComponent( value ); + } + if ( attr ) { attr.set( 'value', value ); } else { attributes_backup[ key ] = value; } } + currentShortcode.set( 'attributes_backup', attributes_backup ); if ( matches[5] ) { diff --git a/js/src/views/edit-shortcode-form.js b/js/src/views/edit-shortcode-form.js index 92dcb00a..6bb13bea 100644 --- a/js/src/views/edit-shortcode-form.js +++ b/js/src/views/edit-shortcode-form.js @@ -1,6 +1,6 @@ var wp = require('wp'), - sui = require('sui-utils/sui'), - backbone = require('backbone'), +sui = require('sui-utils/sui'), +backbone = require('backbone'), editAttributeField = require( 'sui-views/edit-attribute-field' ), // Additional attribute field types: these fields are all standalone in functionality, @@ -27,7 +27,7 @@ var EditShortcodeForm = wp.Backbone.View.extend({ var view = new editAttributeField( { model: innerContent } ); view.shortcode = t.model; - view.template = wp.media.template( 'shortcode-ui-content' ); + view.template = wp.media.template( 'shortcode-ui-content' ); t.views.add( '.edit-shortcode-form-fields', view ); diff --git a/php-tests/test-shortcode-ui.php b/php-tests/test-shortcode-ui.php index eff199a8..cf896c8f 100644 --- a/php-tests/test-shortcode-ui.php +++ b/php-tests/test-shortcode-ui.php @@ -13,6 +13,34 @@ public function test_expected_fields() { $this->assertArrayHasKey( 'post_select', $fields ); } + public function test_filter_shortcode_atts_decode_encoded() { + + $shortcode = 'shortcake_encoding_test'; + + // Add test Shortcode. + add_shortcode( $shortcode, '__return_null' ); + + // Register test Shortcode UI. + shortcode_ui_register_for_shortcode( $shortcode, array( + 'attrs' => array( + array( + 'attr' => 'test', + 'type' => 'text', + 'encode' => true, + ), + ), + ) ); + + $encoded = '%3Cb%20class%3D%22foo%22%3Ebar%3C%2Fb%3E'; + $decoded = 'bar'; + + // Parse shortcode attributes. + $attr = shortcode_atts( array( 'test' => null ), array( 'test' => $encoded ), $shortcode ); + + // Expect value of $attr['test'] to be decoded. + $this->assertEquals( $attr['test'], $decoded ); + } + public function test_register_shortcode_malicious_html() { Shortcode_UI::get_instance()->register_shortcode_ui( 'foo', array( 'inner_content' => array(