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(