diff --git a/.gitignore b/.gitignore index fd4f2b06..825fc67c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules .DS_Store +npm-debug.log diff --git a/.travis.yml b/.travis.yml index 5cd87fb5..f86ca923 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,13 @@ env: - WP_VERSION=latest WP_MULTISITE=0 before_script: - - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - npm install -g grunt-cli + - npm install -script: phpunit +script: + - phpunit + - grunt jasmine; notifications: email: diff --git a/js-tests/build/helpers.js b/js-tests/build/helpers.js new file mode 100644 index 00000000..a88f35ff --- /dev/null +++ b/js-tests/build/helpers.js @@ -0,0 +1,29 @@ +(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;otest content' ); + expect( constructor.fetch ).not.toHaveBeenCalled(); + + // If content is empty - just null and fetch should be called. + constructor.content = null; + expect( constructor.getContent() ).toEqual( null ); + expect( constructor.fetch ).toHaveBeenCalled(); + + } ); + + describe( "Fetch preview HTML", function() { + + beforeEach(function() { + jasmine.Ajax.install(); + }); + + afterEach(function() { + jasmine.Ajax.uninstall(); + }); + + var constructor = jQuery.extend( true, { + render: function( force ) {}, + }, MceViewConstructor ); + + // Mock shortcode model data. + constructor.shortcodeModel = jQuery.extend( true, {}, sui.shortcodes.first() ); + + it( 'Fetches data success', function(){ + + spyOn( wp.ajax, "post" ).and.callThrough(); + spyOn( constructor, "render" ); + + constructor.fetch(); + + expect( constructor.fetching ).toEqual( true ); + expect( constructor.content ).toEqual( undefined ); + expect( wp.ajax.post ).toHaveBeenCalled(); + expect( constructor.render ).not.toHaveBeenCalled(); + + jasmine.Ajax.requests.mostRecent().respondWith( { + 'status': 200, + 'responseText': '{"success":true,"data":"test preview response body"}' + } ); + + expect( constructor.fetching ).toEqual( undefined ); + expect( constructor.content ).toEqual( 'test preview response body' ); + expect( constructor.render ).toHaveBeenCalled(); + + }); + + it( 'Handles errors when fetching data', function() { + + spyOn( constructor, "render" ); + + constructor.fetch(); + + jasmine.Ajax.requests.mostRecent().respondWith( { + 'status': 500, + 'responseText': '{"success":false}' + }); + + expect( constructor.fetching ).toEqual( undefined ); + expect( constructor.content ).toContain( 'shortcake-error' ); + expect( constructor.render ).toHaveBeenCalled(); + + } ); + + } ); + + it( 'parses simple shortcode', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test_shortcode attr="test value"]') + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'attr' }).get('value') ).toEqual( 'test value' ); + }); + + it( 'parses shortcode with content', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test_shortcode attr="test value 1"]test content [/test_shortcode]') + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'attr' }).get('value') ).toEqual( 'test value 1' ); + expect( shortcode.get( 'inner_content' ).get('value') ).toEqual( 'test content ' ); + }); + + it( 'parses shortcode with dashes in name and attribute', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test-shortcode test-attr="test value 2"]') + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).toEqual( 'test value 2' ); + }); + + // https://github.com/fusioneng/Shortcake/issues/171 + xit( 'parses shortcode with line breaks in inner content', function() { + var shortcode = MceViewConstructor.parseShortcodeString( "[test_shortcode]test \ncontent \r2 [/test_shortcode]") + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'inner_content' ).get('value') ).toEqual( "test \ncontent \r2 " ); + } ); + +} ); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../../../js/models/shortcode.js":10,"./../../../js/utils/shortcode-view-constructor.js":11,"./../../../js/utils/sui.js":12}],5:[function(require,module,exports){ +var Shortcodes = require('./../../../js/collections/shortcodes.js'); +var sui = require('./../../../js/utils/sui.js'); + +describe( "SUI Util", function() { + + it( 'sets global variable', function() { + expect( window.Shortcode_UI ).toEqual( sui ); + }); + + it( 'expected properties', function() { + expect( sui.shortcodes instanceof Shortcodes ).toEqual( true ); + expect( sui.views ).toEqual( {} ); + }); + +} ); + +},{"./../../../js/collections/shortcodes.js":7,"./../../../js/utils/sui.js":12}],6:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); +var ShortcodeAttribute = require('./../models/shortcode-attribute.js'); + +/** + * Shortcode Attributes collection. + */ +var ShortcodeAttributes = Backbone.Collection.extend({ + model : ShortcodeAttribute, + // Deep Clone. + clone : function() { + return new this.constructor(_.map(this.models, function(m) { + return m.clone(); + })); + } +}); + +module.exports = ShortcodeAttributes; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../models/shortcode-attribute.js":9}],7:[function(require,module,exports){ +(function (global){ +var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); +var Shortcode = require('./../models/shortcode.js'); + +// Shortcode Collection +var Shortcodes = Backbone.Collection.extend({ + model : Shortcode +}); + +module.exports = Shortcodes; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../models/shortcode.js":10}],8:[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 : {}) +},{}],9:[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: '', + placeholder: '', + }, +}); + +module.exports = ShortcodeAttribute; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],10:[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 ) { + + // Handle content attribute as a special case. + if ( attr.get( 'attr' ) === 'content' ) { + content = attr.get( 'value' ); + } else { + + // Numeric/unnamed attributes + if ( ! isNaN( attr.get( 'attr' ) ) ) { + + // Empty attributes are false to preserve attribute keys + // console.log(attr, attr.get( 'value' )); + attrs.push( typeof attr.get( 'value' ) === 'undefined' || attr.get( 'value' ).trim() === '' ? 'false' : attr.get( 'value' ) ); + + // String attribute names + } else { + + // Skip empty attributes. + if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { + return; + } + + 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":6,"./inner-content.js":8}],11:[function(require,module,exports){ +(function (global){ +sui = require('./sui.js'); +wp = (typeof window !== "undefined" ? window.wp : typeof global !== "undefined" ? global.wp : null); +jQuery = (typeof window !== "undefined" ? window.jQuery : typeof global !== "undefined" ? global.jQuery : null); + +/** + * Generic shortcode mce view constructor. + * This is cloned and used by each shortcode when registering a view. + */ +var shortcodeViewConstructor = { + + initialize: function( options ) { + this.shortcodeModel = this.getShortcodeModel( this.shortcode ); + }, + + /** + * Get the shortcode model given the view shortcode options. + * Must be a registered shortcode (see sui.shortcodes) + */ + getShortcodeModel: function( options ) { + + var shortcodeModel; + + shortcodeModel = sui.shortcodes.findWhere( { shortcode_tag: options.tag } ); + + if ( ! shortcodeModel ) { + return; + } + + shortcodeModel = shortcodeModel.clone(); + + shortcodeModel.get('attrs').each( + function( attr ) { + // console.log('in getShortcodeModel first', attr); + if ( attr.get('attr') in options.attrs.named ) { + attr.set( + 'value', + options.attrs.named[ attr.get('attr') ] + ); + } + + // console.log('in getShortcodeModel', options.shortcode.attrs.numeric); + // if ( typeof options.shortcode.attrs.numeric !== 'undefined' ) { + if ( attr.get( 'attr') in options.shortcode.attrs.numeric ) { + attr.set( + 'value', + options.shortcode.attrs.numeric[ attr.get( 'attr') ] + ); + } + // } + + } + ); + + if ( 'content' in options ) { + var innerContent = shortcodeModel.get('inner_content'); + if ( innerContent ) { + innerContent.set('value', options.content) + } + } + + return shortcodeModel; + + }, + + /** + * Return the preview HTML. + * If empty, fetches data. + * + * @return string + */ + getContent : function() { + if ( ! this.content ) { + this.fetch(); + } + return this.content; + }, + + /** + * Fetch preview. + * Async. Sets this.content and calls this.render. + * + * @return undefined + */ + fetch : function() { + + var self = this; + + if ( ! this.fetching ) { + + this.fetching = true; + + wp.ajax.post( 'do_shortcode', { + post_id: jQuery( '#post_ID' ).val(), + shortcode: this.shortcodeModel.formatShortcode(), + nonce: shortcodeUIData.nonces.preview, + }).done( function( response ) { + self.content = response; + }).fail( function() { + self.content = '' + shortcodeUIData.strings.mce_view_error + ''; + } ).always( function() { + delete self.fetching; + self.render( true ); + } ); + + } + + }, + + /** + * Edit shortcode. + * Get shortcode model and open edit modal. + * + */ + edit : function( shortcodeString ) { + + var currentShortcode; + + // Backwards compatability for WP pre-4.2 + if ( 'object' === typeof( shortcodeString ) ) { + shortcodeString = decodeURIComponent( jQuery(shortcodeString).attr('data-wpview-text') ); + } + + currentShortcode = this.parseShortcodeString( shortcodeString ); + + if ( currentShortcode ) { + + var wp_media_frame = wp.media.frames.wp_media_frame = wp.media({ + frame : "post", + state : 'shortcode-ui', + currentShortcode : currentShortcode, + }); + + wp_media_frame.open(); + + } + + }, + + /** + * Parse a shortcode string and return shortcode model. + * Must be a registered shortcode - see window.Shortcode_UI.shortcodes. + * + * @todo - I think there must be a cleaner way to get the + * shortcode & args here that doesn't use regex. + * + * @param string shortcodeString + * @return Shortcode + */ + parseShortcodeString: function( shortcodeString ) { + + var model, attr; + + var megaRegex = /\[([^\s\]]+)([^\]]+)?\]([^\[]*)?(\[\/(\S+?)\])?/; + var matches = shortcodeString.match( megaRegex ); + // console.log(shortcodeString); + + if ( ! matches ) { + return; + } + + defaultShortcode = sui.shortcodes.findWhere({ + shortcode_tag : matches[1] + }); + + if ( ! defaultShortcode ) { + return; + } + + currentShortcode = defaultShortcode.clone(); + + if ( matches[2] ) { + + // Get all the attributes + attributeMatches = matches[2].match(/([^\s]+)/g) || []; + + // Keep track of all the unnamed attributes + var unnamedIndex = 0; + + // convert attribute strings to object. + // console.log('before find match', attributeMatches.length); + for (var i = 0; i < attributeMatches.length; i++) { + + // Handler for named attributes + if (attributeMatches[i].match(/\S+?="(.*?)"/) !== null ) { + + var bitsRegEx = /(\S+?)="(.*?)"/g; + var bits = bitsRegEx.exec(attributeMatches[i]); + + attr = currentShortcode.get('attrs').findWhere({ + attr : bits[1] + }); + if (attr) { + attr.set('value', bits[2]); + } + + // Handler for numeric/unnamed attributes + } else { + + attr = currentShortcode.get('attrs').findWhere({ + attr : (unnamedIndex++).toString() + }); + // console.log('finding', attr); + if (attr) { + attr.set('value', attributeMatches[i].replace(/^"(.*)"$/, '$1')); + } + + } + + } + + } + + if ( matches[3] ) { + var inner_content = currentShortcode.get( 'inner_content' ); + inner_content.set( 'value', matches[3] ); + } + + return currentShortcode; + + }, + + // Backwards compatability for Pre WP 4.2. + View: { + + overlay: true, + + initialize: function( options ) { + this.shortcode = this.getShortcode( options ); + }, + + getShortcode: function( options ) { + + var shortcodeModel, shortcode; + + shortcodeModel = sui.shortcodes.findWhere( { shortcode_tag: options.shortcode.tag } ); + + if (!shortcodeModel) { + return; + } + + shortcode = shortcodeModel.clone(); + + shortcode.get('attrs').each( + function(attr) { + + if (attr.get('attr') in options.shortcode.attrs.named) { + attr.set('value', + options.shortcode.attrs.named[attr + .get('attr')]); + } + + if ( attr.get( 'attr') in options.shortcode.attrs.numeric ) { + attr.set( + 'value', + options.shortcode.attrs.numeric[ attr.get( 'attr') ] + ); + } + + }); + + if ('content' in options.shortcode) { + var inner_content = shortcode.get('inner_content'); + if ( inner_content ) { + inner_content.set('value', options.shortcode.content) + } + } + + return shortcode; + + }, + + fetch : function() { + + var self = this; + + if ( ! this.parsed ) { + + wp.ajax.post( 'do_shortcode', { + post_id: jQuery( '#post_ID' ).val(), + shortcode: this.shortcode.formatShortcode(), + nonce: shortcodeUIData.nonces.preview, + }).done( function( response ) { + if ( response.indexOf( ''; + self.render( true ); + } ); + + } + + }, + + /** + * Render the shortcode + * + * To ensure consistent rendering - this makes an ajax request to the + * admin and displays. + * + * @return string html + */ + getHtml : function() { + + if ( ! this.parsed ) { + this.fetch(); + } + + return this.parsed; + }, + + /** + * Returns an array of tags for stylesheets applied to the TinyMCE editor. + * + * @method getEditorStyles + * @returns {Array} + */ + getEditorStyles: function() { + + var styles = ''; + + this.getNodes( function ( editor, node, content ) { + var dom = editor.dom, + bodyClasses = editor.getBody().className || '', + iframe, iframeDoc, i, resize; + + tinymce.each( dom.$( 'link[rel="stylesheet"]', editor.getDoc().head ), function( link ) { + if ( link.href && link.href.indexOf( 'skins/lightgray/content.min.css' ) === -1 && + link.href.indexOf( 'skins/wordpress/wp-content.css' ) === -1 ) { + + styles += dom.getOuterHTML( link ) + '\n'; + } + + }); + + } ); + + return styles; + }, + + }, + +}; + +module.exports = shortcodeViewConstructor; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./sui.js":12}],12:[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":7}]},{},[1,2,3,4,5]); diff --git a/js-tests/src/helpers/globalHelper.js b/js-tests/src/helpers/globalHelper.js new file mode 100644 index 00000000..f742ed1b --- /dev/null +++ b/js-tests/src/helpers/globalHelper.js @@ -0,0 +1,26 @@ +// Mock localization data. +window.shortcodeUIData = { + + "shortcodes": [], + + "strings": { + "media_frame_title":"Insert Post Element", + "media_frame_menu_insert_label":"Insert Post Element", + "media_frame_menu_update_label":"Post Element Details", + "media_frame_toolbar_insert_label":"Insert Element", + "media_frame_toolbar_update_label":"Update", + "edit_tab_label":"Edit", + "preview_tab_label":"Preview", + "mce_view_error":"Failed to load preview", + "search_placeholder":"Search", + "insert_content_label":"Inner Content" + }, + + "nonces":{ + "preview": "123", + "thumbnailImage": "123" + } + +}; + +window.shortcodeUIFieldData = {} diff --git a/js-tests/src/innerContentModelSpec.js b/js-tests/src/innerContentModelSpec.js new file mode 100644 index 00000000..3603fe0f --- /dev/null +++ b/js-tests/src/innerContentModelSpec.js @@ -0,0 +1,27 @@ +var InnerContent = require('../../js/models/inner-content'); + +describe( "Shortcode Inner Content Model", function() { + + var data = { + label: 'Inner Content', + type: 'textarea', + value: 'test content', + placeholder: 'test placeholder', + }; + + it( 'sets defaults correctly.', function() { + var content = new InnerContent(); + expect( content.toJSON() ).toEqual( { + label: 'Inner Content', + type: 'textarea', + value: '', + placeholder: '', + } ); + }); + + it( 'sets data correctly.', function() { + var content = new InnerContent( data ); + expect( content.toJSON() ).toEqual( data ); + }); + +} ); diff --git a/js-tests/src/shortcodeAttributeModelSpec.js b/js-tests/src/shortcodeAttributeModelSpec.js new file mode 100644 index 00000000..6161fc07 --- /dev/null +++ b/js-tests/src/shortcodeAttributeModelSpec.js @@ -0,0 +1,20 @@ +var ShortcodeAttribute = require('../../js/models/shortcode-attribute'); + +describe( "Shortcode Attribute Model", function() { + + var attrData = { + attr: 'attr', + label: 'Attribute', + type: 'text', + value: 'test value', + placeholder: 'test placeholder', + }; + + var attr = new ShortcodeAttribute( attrData ); + + it( 'should correctly set data.', function() { + expect( attr.toJSON() ).toEqual( attrData ); + }); + + +} ); diff --git a/js-tests/src/shortcodeModelSpec.js b/js-tests/src/shortcodeModelSpec.js new file mode 100644 index 00000000..e4f27850 --- /dev/null +++ b/js-tests/src/shortcodeModelSpec.js @@ -0,0 +1,68 @@ +var Shortcode = require('../../js/models/shortcode'); +var InnerContent = require('../../js/models/inner-content'); +var ShortcodeAttribute = require('../../js/models/shortcode-attribute'); +var ShortcodeAttributes = require('../../js/collections/shortcode-attributes'); + +describe( "Shortcode Model", function() { + + var defaultShortcode, shortcode, data; + + data = { + label: 'Test Label', + shortcode_tag: 'test_shortcode', + attrs: [ + { + attr: 'attr', + label: 'Attribute', + type: 'text', + value: 'test value', + placeholder: 'test placeholder', + } + ], + inner_content: { + value: 'test content', + }, + }; + + var defaultShortcode = new Shortcode(); + var shortcode = new Shortcode( data ); + + it( 'Defaults set correctly.', function() { + expect( defaultShortcode.get( 'label' ) ).toEqual( '' ); + expect( defaultShortcode.get( 'shortcode_tag' ) ).toEqual( '' ); + expect( defaultShortcode.get( 'attrs' ) instanceof ShortcodeAttributes ).toEqual( true ); + expect( defaultShortcode.get( 'attrs' ).length ).toEqual( 0 ); + expect( defaultShortcode.get( 'inner_content' ) ).toEqual( undefined ); + }); + + it( 'Attribute data set correctly..', function() { + expect( shortcode.get( 'attrs' ) instanceof ShortcodeAttributes ).toEqual( true ); + expect( shortcode.get( 'attrs' ).length ).toEqual( 1 ); + expect( shortcode.get( 'attrs' ).first().get('type') ).toEqual( data.attrs[0].type ); + }); + + it( 'Inner content set correctly..', function() { + expect( shortcode.get( 'inner_content' ) instanceof InnerContent ).toEqual( true ); + expect( shortcode.get( 'inner_content' ).get('value') ).toEqual( data.inner_content.value ); + }); + + it( 'Converted to JSON correctly.', function() { + var json = shortcode.toJSON(); + expect( json.label ).toEqual( data.label ); + expect( json.attrs[0].label ).toEqual( data.attrs[0].label ); + }); + + it( 'Format shortcode.', function() { + + var _shortcode = jQuery.extend( true, {}, shortcode ); + + // Test with attribute and with content. + expect( _shortcode.formatShortcode() ).toEqual( '[test_shortcode attr="test value"]test content[/test_shortcode]' ); + + // Test without content. + _shortcode.get('inner_content').unset( 'value' ); + expect( _shortcode.formatShortcode() ).toEqual( '[test_shortcode attr="test value"]' ); + + }); + +}); diff --git a/js-tests/src/utils/mceViewConstructorSpec.js b/js-tests/src/utils/mceViewConstructorSpec.js new file mode 100644 index 00000000..04a94213 --- /dev/null +++ b/js-tests/src/utils/mceViewConstructorSpec.js @@ -0,0 +1,161 @@ +var Shortcode = require('sui-models/shortcode'); +var MceViewConstructor = require('sui-utils/shortcode-view-constructor'); +var sui = require('sui-utils/sui'); +var jQuery = require('jquery'); +var wp = require('wp'); + +describe( "MCE View Constructor", function() { + + sui.shortcodes.push( new Shortcode( { + label: 'Test Label', + shortcode_tag: 'test_shortcode', + attrs: [ + { + attr: 'attr', + label: 'Attribute', + } + ], + inner_content: { + value: 'test content', + }, + } ) ); + + sui.shortcodes.push( new Shortcode( { + label: 'Test shortcode with dash', + shortcode_tag: 'test-shortcode', + attrs: [ + { + attr: 'test-attr', + label: 'Test attribute with dash', + } + ], + inner_content: { + value: 'test content', + }, + } ) ); + + it ( 'test get shortcode model', function() { + + var constructor = jQuery.extend( true, {}, MceViewConstructor ); + + constructor.shortcode = { + tag: "test_shortcode", + attrs: { + named: { + attr: "Vestibulum ante ipsum primis" + }, + }, + content: "Mauris iaculis porttitor posuere." + }; + + constructor.shortcodeModel = constructor.getShortcodeModel( constructor.shortcode ); + expect( constructor.shortcodeModel instanceof Shortcode ).toEqual( true ); + expect( constructor.shortcodeModel.get( 'attrs' ).first().get( 'value' ) ).toEqual( 'Vestibulum ante ipsum primis' ); + expect( constructor.shortcodeModel.get( 'inner_content' ).get( 'value' ) ).toEqual( 'Mauris iaculis porttitor posuere.' ); + + } ); + + it ( 'test getContent.', function() { + + var constructor = jQuery.extend( true, {}, MceViewConstructor ); + + spyOn( constructor, 'fetch' ); + + // If content is set - just return and don't fetch data. + constructor.content = '

test content

'; + expect( constructor.getContent() ).toEqual( '

test content

' ); + expect( constructor.fetch ).not.toHaveBeenCalled(); + + // If content is empty - just null and fetch should be called. + constructor.content = null; + expect( constructor.getContent() ).toEqual( null ); + expect( constructor.fetch ).toHaveBeenCalled(); + + } ); + + describe( "Fetch preview HTML", function() { + + beforeEach(function() { + jasmine.Ajax.install(); + }); + + afterEach(function() { + jasmine.Ajax.uninstall(); + }); + + var constructor = jQuery.extend( true, { + render: function( force ) {}, + }, MceViewConstructor ); + + // Mock shortcode model data. + constructor.shortcodeModel = jQuery.extend( true, {}, sui.shortcodes.first() ); + + it( 'Fetches data success', function(){ + + spyOn( wp.ajax, "post" ).and.callThrough(); + spyOn( constructor, "render" ); + + constructor.fetch(); + + expect( constructor.fetching ).toEqual( true ); + expect( constructor.content ).toEqual( undefined ); + expect( wp.ajax.post ).toHaveBeenCalled(); + expect( constructor.render ).not.toHaveBeenCalled(); + + jasmine.Ajax.requests.mostRecent().respondWith( { + 'status': 200, + 'responseText': '{"success":true,"data":"test preview response body"}' + } ); + + expect( constructor.fetching ).toEqual( undefined ); + expect( constructor.content ).toEqual( 'test preview response body' ); + expect( constructor.render ).toHaveBeenCalled(); + + }); + + it( 'Handles errors when fetching data', function() { + + spyOn( constructor, "render" ); + + constructor.fetch(); + + jasmine.Ajax.requests.mostRecent().respondWith( { + 'status': 500, + 'responseText': '{"success":false}' + }); + + expect( constructor.fetching ).toEqual( undefined ); + expect( constructor.content ).toContain( 'shortcake-error' ); + expect( constructor.render ).toHaveBeenCalled(); + + } ); + + } ); + + it( 'parses simple shortcode', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test_shortcode attr="test value"]') + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'attr' }).get('value') ).toEqual( 'test value' ); + }); + + it( 'parses shortcode with content', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test_shortcode attr="test value 1"]test content [/test_shortcode]') + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'attr' }).get('value') ).toEqual( 'test value 1' ); + expect( shortcode.get( 'inner_content' ).get('value') ).toEqual( 'test content ' ); + }); + + it( 'parses shortcode with dashes in name and attribute', function() { + var shortcode = MceViewConstructor.parseShortcodeString( '[test-shortcode test-attr="test value 2"]') + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'attrs' ).findWhere( { attr: 'test-attr' }).get('value') ).toEqual( 'test value 2' ); + }); + + // https://github.com/fusioneng/Shortcake/issues/171 + xit( 'parses shortcode with line breaks in inner content', function() { + var shortcode = MceViewConstructor.parseShortcodeString( "[test_shortcode]test \ncontent \r2 [/test_shortcode]") + expect( shortcode instanceof Shortcode ).toEqual( true ); + expect( shortcode.get( 'inner_content' ).get('value') ).toEqual( "test \ncontent \r2 " ); + } ); + +} ); diff --git a/js-tests/src/utils/suiSpec.js b/js-tests/src/utils/suiSpec.js new file mode 100644 index 00000000..eba37962 --- /dev/null +++ b/js-tests/src/utils/suiSpec.js @@ -0,0 +1,15 @@ +var Shortcodes = require('sui-collections/shortcodes'); +var sui = require('sui-utils/sui'); + +describe( "SUI Util", function() { + + it( 'sets global variable', function() { + expect( window.Shortcode_UI ).toEqual( sui ); + }); + + it( 'expected properties', function() { + expect( sui.shortcodes instanceof Shortcodes ).toEqual( true ); + expect( sui.views ).toEqual( {} ); + }); + +} ); diff --git a/js/build/field-attachment.js b/js/build/field-attachment.js new file mode 100644 index 00000000..8e9bb606 --- /dev/null +++ b/js/build/field-attachment.js @@ -0,0 +1,408 @@ +(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 ) { + + jQuery( '', { + src: attachment.icon, + alt: attachment.title, + } ).appendTo( $thumbnail ); + + jQuery( '
', { + class: 'filename', + html: '
' + attachment.title + '
', + } ).appendTo( $thumbnail ); + + } else { + + jQuery( '', { + 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: '', + 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 ) { + + // Handle content attribute as a special case. + if ( attr.get( 'attr' ) === 'content' ) { + content = attr.get( 'value' ); + } else { + + // Numeric/unnamed attributes + if ( ! isNaN( attr.get( 'attr' ) ) ) { + + // Empty attributes are false to preserve attribute keys + // console.log(attr, attr.get( 'value' )); + attrs.push( typeof attr.get( 'value' ) === 'undefined' || attr.get( 'value' ).trim() === '' ? 'false' : attr.get( 'value' ) ); + + // String attribute names + } else { + + // Skip empty attributes. + if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { + return; + } + + 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'); + +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() { + this.$el.html( this.template( this.model.toJSON() ) ); + 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 = jQuery( this.el ).find( '[name=' + this.model.get( 'attr' ) + ']' ); + } else { + var $el = jQuery( 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..0295b42f --- /dev/null +++ b/js/build/field-color.js @@ -0,0 +1,279 @@ +(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'); + +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() { + this.$el.html( this.template( this.model.toJSON() ) ); + 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 = jQuery( this.el ).find( '[name=' + this.model.get( 'attr' ) + ']' ); + } else { + var $el = jQuery( 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 c3f725b8..2bfe1613 100644 --- a/js/build/shortcode-ui.js +++ b/js/build/shortcode-ui.js @@ -186,13 +186,29 @@ Shortcode = Backbone.Model.extend({ this.get( 'attrs' ).each( function( attr ) { - // Skip empty attributes. - if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { - return; + // Handle content attribute as a special case. + if ( attr.get( 'attr' ) === 'content' ) { + content = attr.get( 'value' ); + } else { + + // Numeric/unnamed attributes + if ( ! isNaN( attr.get( 'attr' ) ) ) { + + // Empty attributes are false to preserve attribute keys + attrs.push( typeof attr.get( 'value' ) === 'undefined' || attr.get( 'value' ).trim() === '' ? 'false' : attr.get( 'value' ) ); + + // String attribute names + } else { + + // Skip empty attributes. + if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { + return; + } + + attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); + } } - attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); - } ); if ( 'undefined' !== typeof this.get( 'inner_content' ).get( 'value' ) && this.get( 'inner_content' ).get( 'value').length > 0 ) { @@ -386,6 +402,14 @@ var shortcodeViewConstructor = { options.attrs.named[ attr.get('attr') ] ); } + + if ( attr.get( 'attr') in options.shortcode.attrs.numeric ) { + attr.set( + 'value', + options.shortcode.attrs.numeric[ attr.get( 'attr') ] + ); + } + } ); @@ -463,19 +487,38 @@ var shortcodeViewConstructor = { if (matches[2]) { - attributeMatches = matches[2].match(/(\S+?=".*?")/g) || []; + // Get all the attributes + // attributeMatches = matches[2].match(/([^\s]+)/g) || []; + attributeMatches = matches[2].match(/(\S+?="(.*?)"|([^\s\]]+))/g) || []; - // convert attribute strings to object. - for (var i = 0; i < attributeMatches.length; i++) { + // Keep track of all the unnamed attributes + var unnamedIndex = 0; - var bitsRegEx = /(\S+?)="(.*?)"/g; - var bits = bitsRegEx.exec(attributeMatches[i]); + // convert attribute strings to object. + for (var i = 1; i < attributeMatches.length; i++) { + + // Handler for named attributes + if (attributeMatches[i].match(/\S+?="(.*?)"/) !== null ) { + + var bitsRegEx = /(\S+?)="(.*?)"/g; + var bits = bitsRegEx.exec(attributeMatches[i]); - attr = currentShortcode.get('attrs').findWhere({ - attr : bits[1] - }); - if (attr) { - attr.set('value', bits[2]); + attr = currentShortcode.get('attrs').findWhere({ + attr : bits[1] + }); + if (attr) { + attr.set('value', bits[2]); + } + + // Handler for numeric/unnamed attributes + } else { + + attr = currentShortcode.get('attrs').findWhere({ + attr : (unnamedIndex++).toString() + }); + if (attr) { + attr.set('value', attributeMatches[i].replace(/^"(.*)"$/, '$1')); + } } } @@ -526,6 +569,13 @@ var shortcodeViewConstructor = { options.shortcode.attrs.named[attr .get('attr')]); } + + if ( attr.get( 'attr') in options.shortcode.attrs.numeric ) { + attr.set( + 'value', + options.shortcode.attrs.numeric[ attr.get( 'attr') ] + ); + } }); @@ -806,6 +856,198 @@ module.exports = insertShortcodeList; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./../utils/sui.js":10,"./insert-shortcode-list-item.js":13}],15:[function(require,module,exports){ (function (global){ +var wp = (typeof window !== "undefined" ? window.wp : typeof global !== "undefined" ? global.wp : null), + MediaController = require('./../controllers/media-controller.js'), + Shortcode_UI = require('./shortcode-ui'), + Toolbar = require('./media-toolbar'); + +var postMediaFrame = wp.media.view.MediaFrame.Post; +var mediaFrame = postMediaFrame.extend( { + + initialize: function() { + + postMediaFrame.prototype.initialize.apply( this, arguments ); + + var id = 'shortcode-ui'; + + var opts = { + id : id, + search : true, + router : false, + toolbar : id + '-toolbar', + menu : 'default', + title : shortcodeUIData.strings.media_frame_menu_insert_label, + tabs : [ 'insert' ], + priority: 66, + content : id + '-content-insert', + }; + + if ( 'currentShortcode' in this.options ) { + opts.title = shortcodeUIData.strings.media_frame_menu_update_label; + } + + var controller = new MediaController( opts ); + + if ( 'currentShortcode' in this.options ) { + controller.props.set( 'currentShortcode', arguments[0].currentShortcode ); + controller.props.set( 'action', 'update' ); + } + + this.states.add([ controller]); + + this.on( 'content:render:' + id + '-content-insert', _.bind( this.contentRender, this, 'shortcode-ui', 'insert' ) ); + this.on( 'toolbar:create:' + id + '-toolbar', this.toolbarCreate, this ); + this.on( 'toolbar:render:' + id + '-toolbar', this.toolbarRender, this ); + this.on( 'menu:render:default', this.renderShortcodeUIMenu ); + + }, + + contentRender : function( id, tab ) { + this.content.set( + new Shortcode_UI( { + controller: this, + className: 'clearfix ' + id + '-content ' + id + '-content-' + tab + } ) + ); + }, + + toolbarRender: function( toolbar ) {}, + + toolbarCreate : function( toolbar ) { + var text = shortcodeUIData.strings.media_frame_toolbar_insert_label; + if ( 'currentShortcode' in this.options ) { + text = shortcodeUIData.strings.media_frame_toolbar_update_label; + } + + toolbar.view = new Toolbar( { + controller : this, + items: { + insert: { + text: text, + style: 'primary', + priority: 80, + requires: false, + click: this.insertAction, + } + } + } ); + }, + + renderShortcodeUIMenu: function( view ) { + + // Add a menu separator link. + view.set({ + 'shortcode-ui-separator': new wp.media.View({ + className: 'separator', + priority: 65 + }) + }); + + // Hide menu if editing. + // @todo - fix this. + // This is a hack. + // I just can't work out how to do it properly... + if ( + view.controller.state().props + && view.controller.state().props.get( 'currentShortcode' ) + ) { + window.setTimeout( function() { + view.controller.$el.addClass( 'hide-menu' ); + } ); + } + + }, + + insertAction: function() { + this.controller.state().insert(); + }, + +} ); + +module.exports = mediaFrame; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../controllers/media-controller.js":3,"./media-toolbar":16,"./shortcode-ui":20}],16:[function(require,module,exports){ +(function (global){ +var wp = (typeof window !== "undefined" ? window.wp : typeof global !== "undefined" ? global.wp : null); + +/** + * Toolbar view that extends wp.media.view.Toolbar + * to define cusotm refresh method + */ +var Toolbar = wp.media.view.Toolbar.extend({ + initialize : function() { + _.defaults(this.options, { + requires : false + }); + // Call 'initialize' directly on the parent class. + wp.media.view.Toolbar.prototype.initialize.apply(this, arguments); + }, + + refresh : function() { + var action = this.controller.state().props.get('action'); + if( this.get('insert') ) { + this.get('insert').model.set('disabled', action == 'select'); + } + /** + * call 'refresh' directly on the parent class + */ + wp.media.view.Toolbar.prototype.refresh.apply(this, arguments); + } +}); + +module.exports = Toolbar; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],17:[function(require,module,exports){ +(function (global){ +var wp = (typeof window !== "undefined" ? window.wp : typeof global !== "undefined" ? global.wp : null); +sui = require('./../utils/sui.js'); + +var SearchShortcode = wp.media.view.Search.extend({ + tagName: 'input', + className: 'search', + id: 'media-search-input', + + initialize: function( options ) { + this.shortcodeList = options.shortcodeList; + }, + + attributes: { + type: 'search', + placeholder: shortcodeUIData.strings.search_placeholder + }, + + events: { + 'keyup': 'search', + }, + + /** + * @returns {wp.media.view.Search} Returns itself to allow chaining + */ + render: function() { + this.el.value = this.model.escape('search'); + return this; + }, + + refreshShortcodes: function( shortcodeData ) { + this.shortcodeList.refresh( shortcodeData ); + }, + + search: function( event ) { + if ( event.target.value == '' ) { + this.refreshShortcodes( sui.shortcodes ); + } else { + this.refreshShortcodes( this.controller.search( event.target.value ) ); + } + } +}); + +sui.views.SearchShortcode = SearchShortcode; +module.exports = SearchShortcode; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./../utils/sui.js":10}],18:[function(require,module,exports){ +(function (global){ var wp = (typeof window !== "undefined" ? window.wp : typeof global !== "undefined" ? global.wp : null), MediaController = require('./../controllers/media-controller.js'), Shortcode_UI = require('./shortcode-ui'), @@ -916,7 +1158,7 @@ wp.media.view.MediaFrame.Post = shortcodeFrame.extend({ }); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../controllers/media-controller.js":3,"./../utils/sui.js":10,"./shortcode-ui":17,"./toolbar":19}],16:[function(require,module,exports){ +},{"./../controllers/media-controller.js":3,"./../utils/sui.js":10,"./shortcode-ui":20,"./toolbar":22}],19:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); @@ -1103,7 +1345,7 @@ sui.views.ShortcodePreview = ShortcodePreview; module.exports = ShortcodePreview; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../utils/sui.js":10}],17:[function(require,module,exports){ +},{"./../utils/sui.js":10}],20:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null), insertShortcodeList = require('./insert-shortcode-list.js'), @@ -1211,7 +1453,7 @@ sui.views.Shortcode_UI = Shortcode_UI; module.exports = Shortcode_UI; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../utils/sui.js":10,"./edit-shortcode-form.js":12,"./insert-shortcode-list.js":14,"./shortcode-preview.js":16,"./tabbed-view.js":18}],18:[function(require,module,exports){ +},{"./../utils/sui.js":10,"./edit-shortcode-form.js":12,"./insert-shortcode-list.js":14,"./shortcode-preview.js":19,"./tabbed-view.js":21}],21:[function(require,module,exports){ (function (global){ var Backbone = (typeof window !== "undefined" ? window.Backbone : typeof global !== "undefined" ? global.Backbone : null); @@ -1336,7 +1578,7 @@ var TabbedView = Backbone.View.extend({ sui.views.TabbedView = TabbedView; module.exports = TabbedView; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../utils/sui.js":10}],19:[function(require,module,exports){ +},{"./../utils/sui.js":10}],22:[function(require,module,exports){ (function (global){ var wp = (typeof window !== "undefined" ? window.wp : typeof global !== "undefined" ? global.wp : null); @@ -1368,4 +1610,4 @@ var Toolbar = wp.media.view.Toolbar.extend({ sui.views.Toolbar = Toolbar; module.exports = Toolbar; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"./../utils/sui.js":10}]},{},[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]); +},{"./../utils/sui.js":10}]},{},[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]); diff --git a/js/models/shortcode.js b/js/models/shortcode.js index c8cad603..53a83f5a 100644 --- a/js/models/shortcode.js +++ b/js/models/shortcode.js @@ -66,13 +66,29 @@ Shortcode = Backbone.Model.extend({ this.get( 'attrs' ).each( function( attr ) { - // Skip empty attributes. - if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { - return; + // Handle content attribute as a special case. + if ( attr.get( 'attr' ) === 'content' ) { + content = attr.get( 'value' ); + } else { + + // Numeric/unnamed attributes + if ( ! isNaN( attr.get( 'attr' ) ) ) { + + // Empty attributes are false to preserve attribute keys + attrs.push( typeof attr.get( 'value' ) === 'undefined' || attr.get( 'value' ).trim() === '' ? 'false' : attr.get( 'value' ) ); + + // String attribute names + } else { + + // Skip empty attributes. + if ( ! attr.get( 'value' ) || attr.get( 'value' ).length < 1 ) { + return; + } + + attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); + } } - attrs.push( attr.get( 'attr' ) + '="' + attr.get( 'value' ) + '"' ); - } ); if ( 'undefined' !== typeof this.get( 'inner_content' ).get( 'value' ) && this.get( 'inner_content' ).get( 'value').length > 0 ) { diff --git a/js/utils/shortcode-view-constructor.js b/js/utils/shortcode-view-constructor.js index 9b4d1b0f..08ad4d8f 100644 --- a/js/utils/shortcode-view-constructor.js +++ b/js/utils/shortcode-view-constructor.js @@ -28,6 +28,14 @@ var shortcodeViewConstructor = { options.attrs.named[ attr.get('attr') ] ); } + + if ( attr.get( 'attr') in options.shortcode.attrs.numeric ) { + attr.set( + 'value', + options.shortcode.attrs.numeric[ attr.get( 'attr') ] + ); + } + } ); @@ -105,19 +113,37 @@ var shortcodeViewConstructor = { if (matches[2]) { - attributeMatches = matches[2].match(/(\S+?=".*?")/g) || []; + // Get all the attributes + attributeMatches = matches[2].match(/(\S+?="(.*?)"|([^\s\]]+))/g) || []; - // convert attribute strings to object. - for (var i = 0; i < attributeMatches.length; i++) { + // Keep track of all the unnamed attributes + var unnamedIndex = 0; - var bitsRegEx = /(\S+?)="(.*?)"/g; - var bits = bitsRegEx.exec(attributeMatches[i]); + // convert attribute strings to object. + for (var i = 1; i < attributeMatches.length; i++) { + + // Handler for named attributes + if (attributeMatches[i].match(/\S+?="(.*?)"/) !== null ) { + + var bitsRegEx = /(\S+?)="(.*?)"/g; + var bits = bitsRegEx.exec(attributeMatches[i]); - attr = currentShortcode.get('attrs').findWhere({ - attr : bits[1] - }); - if (attr) { - attr.set('value', bits[2]); + attr = currentShortcode.get('attrs').findWhere({ + attr : bits[1] + }); + if (attr) { + attr.set('value', bits[2]); + } + + // Handler for numeric/unnamed attributes + } else { + + attr = currentShortcode.get('attrs').findWhere({ + attr : (unnamedIndex++).toString() + }); + if (attr) { + attr.set('value', attributeMatches[i].replace(/^"(.*)"$/, '$1')); + } } } @@ -168,6 +194,13 @@ var shortcodeViewConstructor = { options.shortcode.attrs.named[attr .get('attr')]); } + + if ( attr.get( 'attr') in options.shortcode.attrs.numeric ) { + attr.set( + 'value', + options.shortcode.attrs.numeric[ attr.get( 'attr') ] + ); + } }); diff --git a/js/views/media-frame.js b/js/views/media-frame.js new file mode 100644 index 00000000..777a5dea --- /dev/null +++ b/js/views/media-frame.js @@ -0,0 +1,109 @@ +var wp = require('wp'), + MediaController = require('sui-controllers/media-controller'), + Shortcode_UI = require('./shortcode-ui'), + Toolbar = require('./media-toolbar'); + +var postMediaFrame = wp.media.view.MediaFrame.Post; +var mediaFrame = postMediaFrame.extend( { + + initialize: function() { + + postMediaFrame.prototype.initialize.apply( this, arguments ); + + var id = 'shortcode-ui'; + + var opts = { + id : id, + search : true, + router : false, + toolbar : id + '-toolbar', + menu : 'default', + title : shortcodeUIData.strings.media_frame_menu_insert_label, + tabs : [ 'insert' ], + priority: 66, + content : id + '-content-insert', + }; + + if ( 'currentShortcode' in this.options ) { + opts.title = shortcodeUIData.strings.media_frame_menu_update_label; + } + + var controller = new MediaController( opts ); + + if ( 'currentShortcode' in this.options ) { + controller.props.set( 'currentShortcode', arguments[0].currentShortcode ); + controller.props.set( 'action', 'update' ); + } + + this.states.add([ controller]); + + this.on( 'content:render:' + id + '-content-insert', _.bind( this.contentRender, this, 'shortcode-ui', 'insert' ) ); + this.on( 'toolbar:create:' + id + '-toolbar', this.toolbarCreate, this ); + this.on( 'toolbar:render:' + id + '-toolbar', this.toolbarRender, this ); + this.on( 'menu:render:default', this.renderShortcodeUIMenu ); + + }, + + contentRender : function( id, tab ) { + this.content.set( + new Shortcode_UI( { + controller: this, + className: 'clearfix ' + id + '-content ' + id + '-content-' + tab + } ) + ); + }, + + toolbarRender: function( toolbar ) {}, + + toolbarCreate : function( toolbar ) { + var text = shortcodeUIData.strings.media_frame_toolbar_insert_label; + if ( 'currentShortcode' in this.options ) { + text = shortcodeUIData.strings.media_frame_toolbar_update_label; + } + + toolbar.view = new Toolbar( { + controller : this, + items: { + insert: { + text: text, + style: 'primary', + priority: 80, + requires: false, + click: this.insertAction, + } + } + } ); + }, + + renderShortcodeUIMenu: function( view ) { + + // Add a menu separator link. + view.set({ + 'shortcode-ui-separator': new wp.media.View({ + className: 'separator', + priority: 65 + }) + }); + + // Hide menu if editing. + // @todo - fix this. + // This is a hack. + // I just can't work out how to do it properly... + if ( + view.controller.state().props + && view.controller.state().props.get( 'currentShortcode' ) + ) { + window.setTimeout( function() { + view.controller.$el.addClass( 'hide-menu' ); + } ); + } + + }, + + insertAction: function() { + this.controller.state().insert(); + }, + +} ); + +module.exports = mediaFrame; diff --git a/js/views/media-toolbar.js b/js/views/media-toolbar.js new file mode 100644 index 00000000..480b2773 --- /dev/null +++ b/js/views/media-toolbar.js @@ -0,0 +1,28 @@ +var wp = require('wp'); + +/** + * Toolbar view that extends wp.media.view.Toolbar + * to define cusotm refresh method + */ +var Toolbar = wp.media.view.Toolbar.extend({ + initialize : function() { + _.defaults(this.options, { + requires : false + }); + // Call 'initialize' directly on the parent class. + wp.media.view.Toolbar.prototype.initialize.apply(this, arguments); + }, + + refresh : function() { + var action = this.controller.state().props.get('action'); + if( this.get('insert') ) { + this.get('insert').model.set('disabled', action == 'select'); + } + /** + * call 'refresh' directly on the parent class + */ + wp.media.view.Toolbar.prototype.refresh.apply(this, arguments); + } +}); + +module.exports = Toolbar; diff --git a/js/views/search-shortcode.js b/js/views/search-shortcode.js new file mode 100644 index 00000000..15a8f1f7 --- /dev/null +++ b/js/views/search-shortcode.js @@ -0,0 +1,44 @@ +var wp = require('wp'); +sui = require('sui-utils/sui'); + +var SearchShortcode = wp.media.view.Search.extend({ + tagName: 'input', + className: 'search', + id: 'media-search-input', + + initialize: function( options ) { + this.shortcodeList = options.shortcodeList; + }, + + attributes: { + type: 'search', + placeholder: shortcodeUIData.strings.search_placeholder + }, + + events: { + 'keyup': 'search', + }, + + /** + * @returns {wp.media.view.Search} Returns itself to allow chaining + */ + render: function() { + this.el.value = this.model.escape('search'); + return this; + }, + + refreshShortcodes: function( shortcodeData ) { + this.shortcodeList.refresh( shortcodeData ); + }, + + search: function( event ) { + if ( event.target.value == '' ) { + this.refreshShortcodes( sui.shortcodes ); + } else { + this.refreshShortcodes( this.controller.search( event.target.value ) ); + } + } +}); + +sui.views.SearchShortcode = SearchShortcode; +module.exports = SearchShortcode; \ No newline at end of file diff --git a/readme.txt b/readme.txt new file mode 100644 index 00000000..a272d520 --- /dev/null +++ b/readme.txt @@ -0,0 +1,57 @@ +=== Shortcake (Shortcode UI) === +Contributors: mattheu, danielbachhuber, zebulonj, jitendraharpalani, sanchothefat, bfintal, davisshaver +Tags: shortcodes +Requires at least: 4.1 +Tested up to: 4.2 +Stable tag: 0.2.1 +License: GPLv2 or later +License URI: http://www.gnu.org/licenses/gpl-2.0.html + +Shortcake makes using WordPress shortcodes a piece of cake. + +== Description == + +Used alongside `add_shortcode`, Shortcake supplies a user-friendly interface for adding a shortcode to a post, and viewing and editing it from within the content editor. + +Once you've installed the plugin, you'll need to [register UI for your shortcodes](https://github.com/fusioneng/Shortcake/wiki/Registering-Shortcode-UI). For inspiration, check out [examples of Shortcake in the wild](https://github.com/fusioneng/Shortcake/wiki/Shortcode-UI-Examples). + +To report bugs or feature requests, [please use Github issues](https://github.com/fusioneng/Shortcake/issues). + +== Installation == + +Shortcake can be installed like any other WordPress plugin. + +Once you've done so, you'll need to [register the UI for your code](https://github.com/fusioneng/Shortcake/wiki/Registering-Shortcode-UI). + +== Screenshots == + +1. Without Shortcake, shortcodes have a minimal UI. +2. But with Shortcake, TinyMCE will render the shortcode in a TinyMCE view. +3. And add a user-friendly UI to edit shortcode content and attributes. +4. Add new shortcodes to your post through "Add Media". + +== Changelog == + += 0.2.1 (March 18, 2015) = + +* Ensure use of jQuery respects jQuery.noConflict() mode in WP. + += 0.2.0 (March 18, 2015) = + +* JS abstracted using Browserify. +* Enhancements to "Add Post Element" UI: shortcodes sorted alphabetically; search based on label. +* Much easier to select shortcode previews that include iframes. +* WordPress 4.2 compatibility. +* Added color picker to list of potential fields. +* Bug fix: IE11 compatibility. +* Bug fix: Checkbox field can now be unchecked. +* [Full release notes](http://fusion.net/story/105889/shortcake-v0-2-0-js-abstraction-add-post-element-enhancements-inner-content-field/). + += 0.1.0 (December 23, 2014) = + +* Supports all HTML5 input types for form fields. +* Shortcode preview tab within the editing experience. +* Re-labeled the UI around “Post Elements”, which is more descriptive than “Content Items.” +* Many bug fixes. +* [Full release notes](http://next.fusion.net/2014/12/23/shortcake-v0-1-0-live-previews-fieldmanager-integration/). + diff --git a/screenshot-1.png b/screenshot-1.png new file mode 100644 index 00000000..fc23a2b5 Binary files /dev/null and b/screenshot-1.png differ diff --git a/screenshot-2.png b/screenshot-2.png new file mode 100644 index 00000000..b62f6c82 Binary files /dev/null and b/screenshot-2.png differ diff --git a/screenshot-3.png b/screenshot-3.png new file mode 100644 index 00000000..8f49242b Binary files /dev/null and b/screenshot-3.png differ diff --git a/screenshot-4.png b/screenshot-4.png new file mode 100644 index 00000000..383b4d39 Binary files /dev/null and b/screenshot-4.png differ