├── README.md ├── example-thing.js └── example-thing.php /README.md: -------------------------------------------------------------------------------- 1 | How to make a TinyMCE view in WordPress 2 | ============= 3 | 4 | Example Thing! 5 | 6 | In this exercise, we will support a new shortcode: `thing` 7 | 8 | Activate the plugin to see a working example of a shortcode being parsed, edited, updated, and rendered. 9 | 10 | To get started, create a Post in the WordPress editor with the following content: 11 | 12 | `[thing]` 13 | 14 | Click it to edit. 15 | 16 | Note: you need to be running WordPress 3.9 (currently `trunk`) or later 17 | -------------------------------------------------------------------------------- /example-thing.js: -------------------------------------------------------------------------------- 1 | /*globals window, document, $, jQuery, _, Backbone */ 2 | (function ($, _, Backbone) { 3 | "use strict"; 4 | var media = wp.media, 5 | 6 | ThingDetailsController = media.controller.State.extend({ 7 | defaults: { 8 | id: 'thing-details', 9 | title: 'Thing Details!', 10 | toolbar: 'thing-details', 11 | content: 'thing-details', 12 | menu: 'thing-details', 13 | router: false, 14 | priority: 60 15 | }, 16 | 17 | initialize: function( options ) { 18 | this.thing = options.thing; 19 | media.controller.State.prototype.initialize.apply( this, arguments ); 20 | } 21 | }), 22 | 23 | ThingTooController = media.controller.State.extend({ 24 | defaults: { 25 | id: 'thing-too', 26 | title: 'Thing Too!', 27 | router: false, 28 | priority: 60, 29 | toolbar: 'thing-too', 30 | content: 'thing-too', 31 | menu: 'thing-details' 32 | }, 33 | 34 | initialize: function( options ) { 35 | this.thing = options.thing; 36 | media.controller.State.prototype.initialize.apply( this, arguments ); 37 | } 38 | }), 39 | 40 | ThingDetailsView = media.view.Settings.AttachmentDisplay.extend({ 41 | className: 'thing-details', 42 | template: media.template( 'thing-details' ), 43 | prepare: function() { 44 | return _.defaults( { 45 | model: this.model.toJSON() 46 | }, this.options ); 47 | } 48 | }), 49 | 50 | ThingTooView = media.view.Settings.AttachmentDisplay.extend({ 51 | className: 'thing-too', 52 | template: media.template( 'thing-too' ), 53 | prepare: function() { 54 | return _.defaults( { 55 | model: this.model.toJSON() 56 | }, this.options ); 57 | } 58 | }), 59 | 60 | ThingDetailsFrame = media.view.MediaFrame.Select.extend({ 61 | defaults: { 62 | id: 'thing', 63 | url: '', 64 | type: 'link', 65 | title: 'Thing!', 66 | priority: 120 67 | }, 68 | 69 | initialize: function( options ) { 70 | this.thing = new Backbone.Model( options.metadata ); 71 | this.options.selection = new media.model.Selection( this.thing.attachment, { multiple: false } ); 72 | media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments ); 73 | }, 74 | 75 | bindHandlers: function() { 76 | media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments ); 77 | 78 | this.on( 'menu:create:thing-details', this.createMenu, this ); 79 | this.on( 'content:render:thing-details', this.contentDetailsRender, this ); 80 | this.on( 'content:render:thing-too', this.contentTooRender, this ); 81 | this.on( 'menu:render:thing-details', this.menuRender, this ); 82 | this.on( 'toolbar:render:thing-details', this.toolbarRender, this ); 83 | this.on( 'toolbar:render:thing-too', this.toolbarTooRender, this ); 84 | }, 85 | 86 | contentDetailsRender: function() { 87 | var view = new ThingDetailsView({ 88 | controller: this, 89 | model: this.state().thing, 90 | attachment: this.state().thing.attachment 91 | }).render(); 92 | 93 | this.content.set( view ); 94 | }, 95 | 96 | contentTooRender: function() { 97 | var view = new ThingTooView({ 98 | controller: this, 99 | model: this.thing, 100 | attachment: this.thing.attachment 101 | }).render(); 102 | 103 | this.content.set( view ); 104 | }, 105 | 106 | menuRender: function( view ) { 107 | var lastState = this.lastState(), 108 | previous = lastState && lastState.id, 109 | frame = this; 110 | 111 | view.set({ 112 | cancel: { 113 | text: 'Cancel!', 114 | priority: 20, 115 | click: function() { 116 | if ( previous ) { 117 | frame.setState( previous ); 118 | } else { 119 | frame.close(); 120 | } 121 | } 122 | }, 123 | separateCancel: new media.View({ 124 | className: 'separator', 125 | priority: 40 126 | }) 127 | }); 128 | }, 129 | 130 | toolbarRender: function() { 131 | this.toolbar.set( new media.view.Toolbar({ 132 | controller: this, 133 | items: { 134 | button: { 135 | style: 'primary', 136 | text: 'Update Thing!', 137 | priority: 80, 138 | click: function() { 139 | var controller = this.controller; 140 | controller.close(); 141 | controller.state().trigger( 'update', controller.thing.toJSON() ); 142 | controller.setState( controller.options.state ); 143 | controller.reset(); 144 | } 145 | } 146 | } 147 | }) ); 148 | }, 149 | 150 | toolbarTooRender: function() { 151 | this.toolbar.set( new media.view.Toolbar({ 152 | controller: this, 153 | items: { 154 | button: { 155 | style: 'primary', 156 | text: 'Update Thing Too!', 157 | priority: 80, 158 | click: function() { 159 | var controller = this.controller; 160 | controller.state().trigger( 'thing-too', controller.thing.toJSON() ); 161 | controller.setState( controller.options.state ); 162 | controller.reset(); 163 | } 164 | } 165 | } 166 | }) ); 167 | }, 168 | 169 | createStates: function() { 170 | this.states.add([ 171 | new ThingDetailsController( { 172 | thing: this.thing 173 | } ), 174 | 175 | new ThingTooController( { 176 | thing: this.thing 177 | } ) 178 | ]); 179 | } 180 | }), 181 | 182 | thing = { 183 | coerce : media.coerce, 184 | 185 | defaults : { 186 | name : '', 187 | color : '' 188 | }, 189 | 190 | edit : function ( data ) { 191 | var frame, shortcode = wp.shortcode.next( 'thing', data ).shortcode; 192 | 193 | frame = new ThingDetailsFrame({ 194 | frame: 'thing', 195 | state: 'thing-details', 196 | metadata: _.defaults( shortcode.attrs.named, thing.defaults ) 197 | }); 198 | 199 | return frame; 200 | }, 201 | 202 | shortcode : function( model ) { 203 | var self = this, content; 204 | 205 | _.each( thing.defaults, function( value, key ) { 206 | model[ key ] = self.coerce( model, key ); 207 | 208 | if ( value === model[ key ] ) { 209 | delete model[ key ]; 210 | } 211 | }); 212 | 213 | content = model.content; 214 | delete model.content; 215 | 216 | return new wp.shortcode({ 217 | tag: 'thing', 218 | attrs: model, 219 | content: content 220 | }); 221 | } 222 | }; 223 | 224 | wp.mce.views.register( 'thing', { 225 | 226 | View: { 227 | className: 'editor-thing', 228 | 229 | template: media.template( 'editor-thing' ), 230 | 231 | initialize: function( options ) { 232 | this.shortcode = options.shortcode; 233 | }, 234 | 235 | getHtml: function() { 236 | return this.template( _.defaults( 237 | this.shortcode.attrs.named, 238 | thing.defaults 239 | ) ); 240 | } 241 | }, 242 | 243 | edit: function( node ) { 244 | var self = this, frame, data; 245 | 246 | data = window.decodeURIComponent( $( node ).attr('data-wpview-text') ); 247 | frame = thing.edit( data ); 248 | frame.state('thing-details').on( 'update', function( selection ) { 249 | var shortcode = thing.shortcode( selection ).string(); 250 | $( node ).attr( 'data-wpview-text', window.encodeURIComponent( shortcode ) ); 251 | wp.mce.views.refreshView( self, shortcode, true ); 252 | frame.detach(); 253 | }); 254 | frame.open(); 255 | } 256 | 257 | } ); 258 | 259 | }(jQuery, _, Backbone)); -------------------------------------------------------------------------------- /example-thing.php: -------------------------------------------------------------------------------- 1 | 36 | 50 | 51 | 58 | 59 | 62 |