diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index c27f6dfb58..0edcb04938 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -1802,7 +1802,10 @@ function wp_ajax_get_attachment() { */ function wp_ajax_query_attachments() { $query = isset( $_REQUEST['query'] ) ? (array) $_REQUEST['query'] : array(); - $query = array_intersect_key( $query, array_flip( array( 's', 'order', 'orderby', 'posts_per_page', 'paged', 'post_mime_type' ) ) ); + $query = array_intersect_key( $query, array_flip( array( + 's', 'order', 'orderby', 'posts_per_page', 'paged', 'post_mime_type', + 'post_parent', 'post__in', 'post__not_in', + ) ) ); $query['post_type'] = 'attachment'; $query['post_status'] = 'inherit'; diff --git a/wp-admin/js/media-upload.js b/wp-admin/js/media-upload.js index aec6412131..881f907424 100644 --- a/wp-admin/js/media-upload.js +++ b/wp-admin/js/media-upload.js @@ -107,12 +107,23 @@ var tb_position; multiple: true } ) ); - workflow.on( 'update', function( selection ) { + workflow.on( 'update:insert', function( selection ) { this.insert( '\n' + selection.map( function( attachment ) { return wp.media.string.image( attachment ); }).join('\n\n') + '\n' ); }, this ); + workflow.on( 'update:gallery', function( selection ) { + var view = wp.mce.view.get('gallery'), + shortcode; + + if ( ! view ) + return; + + shortcode = view.gallery.shortcode( selection ); + this.insert( shortcode.string() ); + }, this ); + return workflow; }, diff --git a/wp-includes/class-wp-editor.php b/wp-includes/class-wp-editor.php index 89d3c75761..4b6cc21626 100644 --- a/wp-includes/class-wp-editor.php +++ b/wp-includes/class-wp-editor.php @@ -191,7 +191,7 @@ final class _WP_Editors { self::$baseurl = includes_url('js/tinymce'); self::$mce_locale = $mce_locale = ( '' == get_locale() ) ? 'en' : strtolower( substr(get_locale(), 0, 2) ); // only ISO 639-1 $no_captions = (bool) apply_filters( 'disable_captions', '' ); - $plugins = array( 'inlinepopups', 'spellchecker', 'tabfocus', 'paste', 'media', 'fullscreen', 'wordpress', 'wpgallery', 'wplink', 'wpdialogs', 'wpview' ); + $plugins = array( 'inlinepopups', 'spellchecker', 'tabfocus', 'paste', 'media', 'fullscreen', 'wordpress', 'wplink', 'wpdialogs', 'wpview' ); $first_run = true; $ext_plugins = ''; diff --git a/wp-includes/js/mce-view.js b/wp-includes/js/mce-view.js index 4ed0310394..c1b08a574b 100644 --- a/wp-includes/js/mce-view.js +++ b/wp-includes/js/mce-view.js @@ -117,7 +117,7 @@ if ( typeof wp === 'undefined' ) shortcode: { view: Backbone.View, text: function( instance ) { - return instance.options.shortcode.text(); + return instance.options.shortcode.string(); }, toView: function( content ) { @@ -503,4 +503,104 @@ if ( typeof wp === 'undefined' ) } } }); + + mceview.add( 'gallery', { + shortcode: 'gallery', + + gallery: (function() { + var galleries = {}; + + return { + attachments: function( shortcode, parent ) { + var shortcodeString = shortcode.string(), + result = galleries[ shortcodeString ], + attrs, args; + + delete galleries[ shortcodeString ]; + + if ( result ) + return result; + + attrs = shortcode.attrs.named; + args = _.pick( attrs, 'orderby', 'order' ); + + args.type = 'image'; + args.perPage = -1; + + // Map the `ids` param to the correct query args. + if ( attrs.ids ) { + args.post__in = attrs.ids.split(','); + args.orderby = 'post__in'; + } else if ( attrs.include ) { + args.post__in = attrs.include.split(','); + } + + if ( attrs.exclude ) + args.post__not_in = attrs.exclude.split(','); + + if ( ! args.post__in ) + args.parent = attrs.id || parent; + + return media.query( args ); + }, + + shortcode: function( attachments ) { + var attrs = _.pick( attachments.props.toJSON(), 'include', 'exclude', 'orderby', 'order' ), + shortcode; + + attrs.ids = attachments.pluck('id'); + + shortcode = new wp.shortcode({ + tag: 'gallery', + attrs: attrs, + type: 'single' + }); + + galleries[ shortcode.string() ] = attachments; + return shortcode; + } + }; + }()), + + view: { + className: 'editor-gallery', + template: media.template('editor-gallery'), + + // The fallback post ID to use as a parent for galleries that don't + // specify the `ids` or `include` parameters. + // + // Uses the hidden input on the edit posts page by default. + parent: $('#post_ID').val(), + + events: { + 'click .close': 'remove' + }, + + initialize: function() { + var view = mceview.get('gallery'), + shortcode = this.options.shortcode; + + this.attachments = view.gallery.attachments( shortcode, this.parent ); + this.attachments.more().done( _.bind( this.render, this ) ); + }, + + render: function() { + var options, thumbnail, size; + + if ( ! this.attachments.length ) + return; + + thumbnail = this.attachments.first().toJSON(); + size = thumbnail.sizes && thumbnail.sizes.thumbnail ? thumbnail.sizes.thumbnail : thumbnail; + + options = { + url: size.url, + orientation: size.orientation, + count: this.attachments.length + }; + + this.$el.html( this.template( options ) ); + } + } + }); }(jQuery)); \ No newline at end of file diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js index 1dc88392e5..aff6596e55 100644 --- a/wp-includes/js/media-models.js +++ b/wp-includes/js/media-models.js @@ -227,9 +227,7 @@ if ( typeof wp === 'undefined' ) this.props.on( 'change:type', this._changeType, this ); // Set the `props` model and fill the default property values. - this.props.set( _.defaults( options.props || {}, { - order: 'DESC' - }) ); + this.props.set( _.defaults( options.props || {} ) ); // Observe another `Attachments` collection if one is provided. if ( options.observe ) @@ -248,7 +246,7 @@ if ( typeof wp === 'undefined' ) if ( this.comparator && this.comparator !== Attachments.comparator ) return; - if ( orderby ) + if ( orderby && 'post__in' !== orderby ) this.comparator = Attachments.comparator; else delete this.comparator; @@ -347,6 +345,7 @@ if ( typeof wp === 'undefined' ) more: function( options ) { if ( this.mirroring && this.mirroring.more ) return this.mirroring.more( options ); + return $.Deferred().resolve().promise(); }, parse: function( resp, xhr ) { @@ -363,7 +362,7 @@ if ( typeof wp === 'undefined' ) }, { comparator: function( a, b ) { var key = this.props.get('orderby'), - order = this.props.get('order'), + order = this.props.get('order') || 'DESC', ac = a.cid, bc = b.cid; @@ -423,6 +422,8 @@ if ( typeof wp === 'undefined' ) */ Query = media.model.Query = Attachments.extend({ initialize: function( models, options ) { + var allowed; + options = options || {}; Attachments.prototype.initialize.apply( this, arguments ); @@ -451,20 +452,28 @@ if ( typeof wp === 'undefined' ) return false; }; - this.observe( Attachments.all ); + // Observe the central `Attachments.all` model to watch for new + // matches for the query. + // + // Only observe when a limited number of query args are set. There + // are no filters for other properties, so observing will result in + // false positives in those queries. + allowed = [ 's', 'order', 'orderby', 'posts_per_page', 'post_mime_type' ]; + if ( _( this.args ).chain().keys().difference().isEmpty().value() ) + this.observe( Attachments.all ); }, more: function( options ) { var query = this; if ( ! this.hasMore ) - return; + return $.Deferred().resolve().promise(); options = options || {}; options.add = true; return this.fetch( options ).done( function( resp ) { - if ( _.isEmpty( resp ) || resp.length < this.args.posts_per_page ) + if ( _.isEmpty( resp ) || -1 === this.args.posts_per_page || resp.length < this.args.posts_per_page ) query.hasMore = false; }); }, @@ -484,7 +493,8 @@ if ( typeof wp === 'undefined' ) args = _.clone( this.args ); // Determine which page to query. - args.paged = Math.floor( this.length / args.posts_per_page ) + 1; + if ( -1 !== args.posts_per_page ) + args.paged = Math.floor( this.length / args.posts_per_page ) + 1; options.data.query = args; return media.ajax( options ); @@ -506,7 +516,7 @@ if ( typeof wp === 'undefined' ) }, orderby: { - allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id' ], + allowed: [ 'name', 'author', 'date', 'title', 'modified', 'uploadedTo', 'id', 'post__in' ], valuemap: { 'id': 'ID', 'uploadedTo': 'parent' @@ -514,8 +524,10 @@ if ( typeof wp === 'undefined' ) }, propmap: { - 'search': 's', - 'type': 'post_mime_type' + 'search': 's', + 'type': 'post_mime_type', + 'parent': 'post_parent', + 'perPage': 'posts_per_page' }, // Caches query objects so queries can be easily reused. diff --git a/wp-includes/js/media-views.js b/wp-includes/js/media-views.js index 045e12ddd7..06936c2f66 100644 --- a/wp-includes/js/media-views.js +++ b/wp-includes/js/media-views.js @@ -117,9 +117,10 @@ return this; }, - update: function() { + update: function( event ) { this.close(); this.trigger( 'update', this.selection ); + this.trigger( 'update:' + event, this.selection ); this.selection.clear(); }, @@ -630,7 +631,7 @@ 'insert-into-post': { text: l10n.insertIntoPost, priority: 30, - click: _.bind( controller.update, controller ) + click: _.bind( controller.update, controller, 'insert' ) }, 'add-to-gallery': { @@ -698,7 +699,7 @@ style: 'primary', text: l10n.insertGalleryIntoPost, priority: 40, - click: _.bind( controller.update, controller ) + click: _.bind( controller.update, controller, 'gallery' ) }, 'add-images-from-library': { diff --git a/wp-includes/js/shortcode.js b/wp-includes/js/shortcode.js index 98c05fa7c0..427efe0ebf 100644 --- a/wp-includes/js/shortcode.js +++ b/wp-includes/js/shortcode.js @@ -8,7 +8,7 @@ if ( typeof wp === 'undefined' ) wp.shortcode = { // ### Find the next matching shortcode // - // Given a shortcode `tag`, a block of `text, and an optional starting + // Given a shortcode `tag`, a block of `text`, and an optional starting // `index`, returns the next matching shortcode or `undefined`. // // Shortcodes are formatted as an object that contains the match @@ -24,13 +24,13 @@ if ( typeof wp === 'undefined' ) return; // If we matched an escaped shortcode, try again. - if ( match[1] === '[' && match[6] === ']' ) + if ( match[1] === '[' && match[7] === ']' ) return wp.shortcode.next( tag, text, re.lastIndex ); result = { index: match.index, content: match[0], - shortcode: new wp.shortcode.Match( match ) + shortcode: wp.shortcode.fromMatch( match ) }; // If we matched a leading `[`, strip it from the match @@ -41,13 +41,13 @@ if ( typeof wp === 'undefined' ) } // If we matched a trailing `]`, strip it from the match. - if ( match[6] ) + if ( match[7] ) result.match = result.match.slice( 0, -1 ); return result; }, - // ### Replace matching shortcodes in a block of text. + // ### Replace matching shortcodes in a block of text // // Accepts a shortcode `tag`, content `text` to scan, and a `callback` // to process the shortcode matches and return a replacement string. @@ -57,14 +57,14 @@ if ( typeof wp === 'undefined' ) // a shortcode `attrs` object, the `content` between shortcode tags, // and a boolean flag to indicate if the match was a `single` tag. replace: function( tag, text, callback ) { - return text.replace( wp.shortcode.regexp( tag ), function( match, left, tag, attrs, closing, content, right, offset ) { + return text.replace( wp.shortcode.regexp( tag ), function( match, left, tag, attrs, slash, content, closing, right, offset ) { // If both extra brackets exist, the shortcode has been // properly escaped. if ( left === '[' && right === ']' ) return match; // Create the match object and pass it through the callback. - var result = callback( new wp.shortcode.Match( arguments ) ); + var result = callback( wp.shortcode.fromMatch( arguments ) ); // Make sure to return any of the extra brackets if they // weren't used to escape the shortcode. @@ -72,7 +72,19 @@ if ( typeof wp === 'undefined' ) }); }, - // ### Generate a shortcode RegExp. + // ### Generate a string from shortcode parameters + // + // Creates a `wp.shortcode` instance and returns a string. + // + // Accepts the same `options` as the `wp.shortcode()` constructor, + // containing a `tag` string, a string or object of `attrs`, a boolean + // indicating whether to format the shortcode using a `single` tag, and a + // `content` string. + string: function( options ) { + return new wp.shortcode( options ).string(); + }, + + // ### Generate a RegExp to identify a shortcode // // The base regex is functionally equivalent to the one found in // `get_shortcode_regex()` in `wp-includes/shortcodes.php`. @@ -84,13 +96,14 @@ if ( typeof wp === 'undefined' ) // 3. The shortcode argument list // 4. The self closing `/` // 5. The content of a shortcode when it wraps some content. - // 6. An extra `]` to allow for escaping shortcodes with double `[[]]` + // 6. The closing tag. + // 7. An extra `]` to allow for escaping shortcodes with double `[[]]` regexp: _.memoize( function( tag ) { - return new RegExp( '\\[(\\[?)(' + tag + ')\\b([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*(?:\\[(?!\\/\\2\\])[^\\[]*)*)\\[\\/\\2\\])?)(\\]?)', 'g' ); + return new RegExp( '\\[(\\[?)(' + tag + ')\\b([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*(?:\\[(?!\\/\\2\\])[^\\[]*)*)(\\[\\/\\2\\]))?)(\\]?)', 'g' ); }), - // ### Parse shortcode attributes. + // ### Parse shortcode attributes // // Shortcodes accept many types of attributes. These can chiefly be // divided into named and numeric attributes: @@ -143,29 +156,74 @@ if ( typeof wp === 'undefined' ) named: named, numeric: numeric }; - }) + }), + + // ### Generate a Shortcode Object from a RegExp match + // Accepts a `match` object from calling `regexp.exec()` on a `RegExp` + // generated by `wp.shortcode.regexp()`. `match` can also be set to the + // `arguments` from a callback passed to `regexp.replace()`. + fromMatch: function( match ) { + var type; + + if ( match[4] ) + type = 'self-closing'; + else if ( match[6] ) + type = 'closed'; + else + type = 'single'; + + return new wp.shortcode({ + tag: match[2], + attrs: match[3], + type: type, + content: match[5] + }); + } }; - // Shortcode Matches + // Shortcode Objects // ----------------- // - // Shortcode matches are generated automatically when using - // `wp.shortcode.next()` and `wp.shortcode.replace()`. These two methods - // should handle most shortcode needs. + // Shortcode objects are generated automatically when using the main + // `wp.shortcode` methods: `next()`, `replace()`, and `string()`. // - // Accepts a `match` object from calling `regexp.exec()` on a `RegExp` - // generated by `wp.shortcode.regexp()`. `match` can also be set to the - // `arguments` from a callback passed to `regexp.replace()`. - wp.shortcode.Match = function( match ) { - this.tag = match[2]; - this.attrs = wp.shortcode.attrs( match[3] ); - this.single = !! match[4]; - this.content = match[5]; - }; + // To access a raw representation of a shortcode, pass an `options` object, + // containing a `tag` string, a string or object of `attrs`, a string + // indicating the `type` of the shortcode ('single', 'self-closing', or + // 'closed'), and a `content` string. + wp.shortcode = _.extend( function( options ) { + _.extend( this, _.pick( options || {}, 'tag', 'attrs', 'type', 'content' ) ); - _.extend( wp.shortcode.Match.prototype, { - // ### Get a shortcode attribute. + var attrs = this.attrs; + + // Ensure we have a correctly formatted `attrs` object. + this.attrs = { + named: {}, + numeric: [] + }; + + if ( ! attrs ) + return; + + // Parse a string of attributes. + if ( _.isString( attrs ) ) { + this.attrs = wp.shortcode.attrs( attrs ); + + // Identify a correctly formatted `attrs` object. + } else if ( _.isEqual( _.keys( attrs ), [ 'named', 'numeric' ] ) ) { + this.attrs = attrs; + + // Handle a flat object of attributes. + } else { + _.each( options.attrs, function( value, key ) { + this.set( key, value ); + }, this ); + } + }, wp.shortcode ); + + _.extend( wp.shortcode.prototype, { + // ### Get a shortcode attribute // // Automatically detects whether `attr` is named or numeric and routes // it accordingly. @@ -173,7 +231,7 @@ if ( typeof wp === 'undefined' ) return this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ]; }, - // ### Set a shortcode attribute. + // ### Set a shortcode attribute // // Automatically detects whether `attr` is named or numeric and routes // it accordingly. @@ -182,8 +240,8 @@ if ( typeof wp === 'undefined' ) return this; }, - // ### Transform the shortcode match into text. - text: function() { + // ### Transform the shortcode match into a string + string: function() { var text = '[' + this.tag; _.each( this.attrs.numeric, function( value ) { @@ -197,9 +255,11 @@ if ( typeof wp === 'undefined' ) text += ' ' + name + '="' + value + '"'; }); - // If the tag is marked as singular, self-close the tag and - // ignore any additional content. - if ( this.single ) + // If the tag is marked as `single` or `self-closing`, close the + // tag and ignore any additional content. + if ( 'single' === this.type ) + return text + ']'; + else if ( 'self-closing' === this.type ) return text + ' /]'; // Complete the opening tag. diff --git a/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css b/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css index 8d6fd1e44a..ed8fe5ae10 100644 --- a/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css +++ b/wp-includes/js/tinymce/themes/advanced/skins/wp_theme/content.css @@ -144,34 +144,21 @@ img.wp-oembed { /* WordPress TinyMCE Previews */ div.wp-view-wrap, div.wp-view { + position: relative; display: inline-block; } -.spinner { - background: #fff url("../../../../../../../wp-admin/images/wpspin_light.gif") no-repeat center center; - border: 1px solid #dfdfdf; - margin-top: 10px; - margin-right: 10px; -} - -.editor-attachment { - position: relative; - padding: 5px; -} - -.editor-attachment, -.editor-attachment img { - min-height: 100px; - min-width: 100px; -} - -.editor-attachment img { +div.wp-view-wrap img { display: block; border: 0; padding: 0; margin: 0; } +.spinner { + background: #fff url("../../../../../../../wp-admin/images/wpspin_light.gif") no-repeat center center; +} + .close { display: none; position: absolute; @@ -187,6 +174,38 @@ div.wp-view { background: #fff; } -.editor-attachment:hover .close { +.editor-attachment:hover .close, +.editor-gallery:hover .close { display: block; +} + +.editor-attachment { + position: relative; + margin-top: 10px; + margin-right: 10px; + padding: 4px; + border: 1px solid #dfdfdf; +} + +.editor-attachment, +.editor-attachment img { + min-height: 100px; + min-width: 100px; +} + +.editor-gallery { + min-height: 150px; + min-width: 150px; + margin: 1px; + border: 4px solid #fff; + box-shadow: + 0 0 0 1px #ccc, + 5px 5px 0 0 #fff, + 5px 5px 0 1px #ccc, + 10px 10px 0 0 #fff, + 10px 10px 0 1px #ccc; +} +.editor-gallery .close { + top: 1px; + right: 1px; } \ No newline at end of file diff --git a/wp-includes/media.php b/wp-includes/media.php index 1fc438385c..f7ec75474d 100644 --- a/wp-includes/media.php +++ b/wp-includes/media.php @@ -1371,5 +1371,12 @@ function wp_print_media_templates( $attachment ) {
×
+ +