media-editor.js 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  1. /* global getUserSetting, tinymce, QTags */
  2. // WordPress, TinyMCE, and Media
  3. // -----------------------------
  4. (function($, _){
  5. /**
  6. * Stores the editors' `wp.media.controller.Frame` instances.
  7. *
  8. * @static
  9. */
  10. var workflows = {};
  11. /**
  12. * A helper mixin function to avoid truthy and falsey values being
  13. * passed as an input that expects booleans. If key is undefined in the map,
  14. * but has a default value, set it.
  15. *
  16. * @param {object} attrs Map of props from a shortcode or settings.
  17. * @param {string} key The key within the passed map to check for a value.
  18. * @returns {mixed|undefined} The original or coerced value of key within attrs
  19. */
  20. wp.media.coerce = function ( attrs, key ) {
  21. if ( _.isUndefined( attrs[ key ] ) && ! _.isUndefined( this.defaults[ key ] ) ) {
  22. attrs[ key ] = this.defaults[ key ];
  23. } else if ( 'true' === attrs[ key ] ) {
  24. attrs[ key ] = true;
  25. } else if ( 'false' === attrs[ key ] ) {
  26. attrs[ key ] = false;
  27. }
  28. return attrs[ key ];
  29. };
  30. /** @namespace wp.media.string */
  31. wp.media.string = {
  32. /**
  33. * Joins the `props` and `attachment` objects,
  34. * outputting the proper object format based on the
  35. * attachment's type.
  36. *
  37. * @param {Object} [props={}] Attachment details (align, link, size, etc).
  38. * @param {Object} attachment The attachment object, media version of Post.
  39. * @returns {Object} Joined props
  40. */
  41. props: function( props, attachment ) {
  42. var link, linkUrl, size, sizes,
  43. defaultProps = wp.media.view.settings.defaultProps;
  44. props = props ? _.clone( props ) : {};
  45. if ( attachment && attachment.type ) {
  46. props.type = attachment.type;
  47. }
  48. if ( 'image' === props.type ) {
  49. props = _.defaults( props || {}, {
  50. align: defaultProps.align || getUserSetting( 'align', 'none' ),
  51. size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ),
  52. url: '',
  53. classes: []
  54. });
  55. }
  56. // All attachment-specific settings follow.
  57. if ( ! attachment ) {
  58. return props;
  59. }
  60. props.title = props.title || attachment.title;
  61. link = props.link || defaultProps.link || getUserSetting( 'urlbutton', 'file' );
  62. if ( 'file' === link || 'embed' === link ) {
  63. linkUrl = attachment.url;
  64. } else if ( 'post' === link ) {
  65. linkUrl = attachment.link;
  66. } else if ( 'custom' === link ) {
  67. linkUrl = props.linkUrl;
  68. }
  69. props.linkUrl = linkUrl || '';
  70. // Format properties for images.
  71. if ( 'image' === attachment.type ) {
  72. props.classes.push( 'wp-image-' + attachment.id );
  73. sizes = attachment.sizes;
  74. size = sizes && sizes[ props.size ] ? sizes[ props.size ] : attachment;
  75. _.extend( props, _.pick( attachment, 'align', 'caption', 'alt' ), {
  76. width: size.width,
  77. height: size.height,
  78. src: size.url,
  79. captionId: 'attachment_' + attachment.id
  80. });
  81. } else if ( 'video' === attachment.type || 'audio' === attachment.type ) {
  82. _.extend( props, _.pick( attachment, 'title', 'type', 'icon', 'mime' ) );
  83. // Format properties for non-images.
  84. } else {
  85. props.title = props.title || attachment.filename;
  86. props.rel = props.rel || 'attachment wp-att-' + attachment.id;
  87. }
  88. return props;
  89. },
  90. /**
  91. * Create link markup that is suitable for passing to the editor
  92. *
  93. * @param {Object} props Attachment details (align, link, size, etc).
  94. * @param {Object} attachment The attachment object, media version of Post.
  95. * @returns {string} The link markup
  96. */
  97. link: function( props, attachment ) {
  98. var options;
  99. props = wp.media.string.props( props, attachment );
  100. options = {
  101. tag: 'a',
  102. content: props.title,
  103. attrs: {
  104. href: props.linkUrl
  105. }
  106. };
  107. if ( props.rel ) {
  108. options.attrs.rel = props.rel;
  109. }
  110. return wp.html.string( options );
  111. },
  112. /**
  113. * Create an Audio shortcode string that is suitable for passing to the editor
  114. *
  115. * @param {Object} props Attachment details (align, link, size, etc).
  116. * @param {Object} attachment The attachment object, media version of Post.
  117. * @returns {string} The audio shortcode
  118. */
  119. audio: function( props, attachment ) {
  120. return wp.media.string._audioVideo( 'audio', props, attachment );
  121. },
  122. /**
  123. * Create a Video shortcode string that is suitable for passing to the editor
  124. *
  125. * @param {Object} props Attachment details (align, link, size, etc).
  126. * @param {Object} attachment The attachment object, media version of Post.
  127. * @returns {string} The video shortcode
  128. */
  129. video: function( props, attachment ) {
  130. return wp.media.string._audioVideo( 'video', props, attachment );
  131. },
  132. /**
  133. * Helper function to create a media shortcode string
  134. *
  135. * @access private
  136. *
  137. * @param {string} type The shortcode tag name: 'audio' or 'video'.
  138. * @param {Object} props Attachment details (align, link, size, etc).
  139. * @param {Object} attachment The attachment object, media version of Post.
  140. * @returns {string} The media shortcode
  141. */
  142. _audioVideo: function( type, props, attachment ) {
  143. var shortcode, html, extension;
  144. props = wp.media.string.props( props, attachment );
  145. if ( props.link !== 'embed' )
  146. return wp.media.string.link( props );
  147. shortcode = {};
  148. if ( 'video' === type ) {
  149. if ( attachment.image && -1 === attachment.image.src.indexOf( attachment.icon ) ) {
  150. shortcode.poster = attachment.image.src;
  151. }
  152. if ( attachment.width ) {
  153. shortcode.width = attachment.width;
  154. }
  155. if ( attachment.height ) {
  156. shortcode.height = attachment.height;
  157. }
  158. }
  159. extension = attachment.filename.split('.').pop();
  160. if ( _.contains( wp.media.view.settings.embedExts, extension ) ) {
  161. shortcode[extension] = attachment.url;
  162. } else {
  163. // Render unsupported audio and video files as links.
  164. return wp.media.string.link( props );
  165. }
  166. html = wp.shortcode.string({
  167. tag: type,
  168. attrs: shortcode
  169. });
  170. return html;
  171. },
  172. /**
  173. * Create image markup, optionally with a link and/or wrapped in a caption shortcode,
  174. * that is suitable for passing to the editor
  175. *
  176. * @param {Object} props Attachment details (align, link, size, etc).
  177. * @param {Object} attachment The attachment object, media version of Post.
  178. * @returns {string}
  179. */
  180. image: function( props, attachment ) {
  181. var img = {},
  182. options, classes, shortcode, html;
  183. props.type = 'image';
  184. props = wp.media.string.props( props, attachment );
  185. classes = props.classes || [];
  186. img.src = ! _.isUndefined( attachment ) ? attachment.url : props.url;
  187. _.extend( img, _.pick( props, 'width', 'height', 'alt' ) );
  188. // Only assign the align class to the image if we're not printing
  189. // a caption, since the alignment is sent to the shortcode.
  190. if ( props.align && ! props.caption ) {
  191. classes.push( 'align' + props.align );
  192. }
  193. if ( props.size ) {
  194. classes.push( 'size-' + props.size );
  195. }
  196. img['class'] = _.compact( classes ).join(' ');
  197. // Generate `img` tag options.
  198. options = {
  199. tag: 'img',
  200. attrs: img,
  201. single: true
  202. };
  203. // Generate the `a` element options, if they exist.
  204. if ( props.linkUrl ) {
  205. options = {
  206. tag: 'a',
  207. attrs: {
  208. href: props.linkUrl
  209. },
  210. content: options
  211. };
  212. }
  213. html = wp.html.string( options );
  214. // Generate the caption shortcode.
  215. if ( props.caption ) {
  216. shortcode = {};
  217. if ( img.width ) {
  218. shortcode.width = img.width;
  219. }
  220. if ( props.captionId ) {
  221. shortcode.id = props.captionId;
  222. }
  223. if ( props.align ) {
  224. shortcode.align = 'align' + props.align;
  225. }
  226. html = wp.shortcode.string({
  227. tag: 'caption',
  228. attrs: shortcode,
  229. content: html + ' ' + props.caption
  230. });
  231. }
  232. return html;
  233. }
  234. };
  235. wp.media.embed = {
  236. coerce : wp.media.coerce,
  237. defaults : {
  238. url : '',
  239. width: '',
  240. height: ''
  241. },
  242. edit : function( data, isURL ) {
  243. var frame, props = {}, shortcode;
  244. if ( isURL ) {
  245. props.url = data.replace(/<[^>]+>/g, '');
  246. } else {
  247. shortcode = wp.shortcode.next( 'embed', data ).shortcode;
  248. props = _.defaults( shortcode.attrs.named, this.defaults );
  249. if ( shortcode.content ) {
  250. props.url = shortcode.content;
  251. }
  252. }
  253. frame = wp.media({
  254. frame: 'post',
  255. state: 'embed',
  256. metadata: props
  257. });
  258. return frame;
  259. },
  260. shortcode : function( model ) {
  261. var self = this, content;
  262. _.each( this.defaults, function( value, key ) {
  263. model[ key ] = self.coerce( model, key );
  264. if ( value === model[ key ] ) {
  265. delete model[ key ];
  266. }
  267. });
  268. content = model.url;
  269. delete model.url;
  270. return new wp.shortcode({
  271. tag: 'embed',
  272. attrs: model,
  273. content: content
  274. });
  275. }
  276. };
  277. /**
  278. * @class wp.media.collection
  279. *
  280. * @param {Object} attributes
  281. */
  282. wp.media.collection = function(attributes) {
  283. var collections = {};
  284. return _.extend(/** @lends wp.media.collection.prototype */{
  285. coerce : wp.media.coerce,
  286. /**
  287. * Retrieve attachments based on the properties of the passed shortcode
  288. *
  289. * @param {wp.shortcode} shortcode An instance of wp.shortcode().
  290. * @returns {wp.media.model.Attachments} A Backbone.Collection containing
  291. * the media items belonging to a collection.
  292. * The query[ this.tag ] property is a Backbone.Model
  293. * containing the 'props' for the collection.
  294. */
  295. attachments: function( shortcode ) {
  296. var shortcodeString = shortcode.string(),
  297. result = collections[ shortcodeString ],
  298. attrs, args, query, others, self = this;
  299. delete collections[ shortcodeString ];
  300. if ( result ) {
  301. return result;
  302. }
  303. // Fill the default shortcode attributes.
  304. attrs = _.defaults( shortcode.attrs.named, this.defaults );
  305. args = _.pick( attrs, 'orderby', 'order' );
  306. args.type = this.type;
  307. args.perPage = -1;
  308. // Mark the `orderby` override attribute.
  309. if ( undefined !== attrs.orderby ) {
  310. attrs._orderByField = attrs.orderby;
  311. }
  312. if ( 'rand' === attrs.orderby ) {
  313. attrs._orderbyRandom = true;
  314. }
  315. // Map the `orderby` attribute to the corresponding model property.
  316. if ( ! attrs.orderby || /^menu_order(?: ID)?$/i.test( attrs.orderby ) ) {
  317. args.orderby = 'menuOrder';
  318. }
  319. // Map the `ids` param to the correct query args.
  320. if ( attrs.ids ) {
  321. args.post__in = attrs.ids.split(',');
  322. args.orderby = 'post__in';
  323. } else if ( attrs.include ) {
  324. args.post__in = attrs.include.split(',');
  325. }
  326. if ( attrs.exclude ) {
  327. args.post__not_in = attrs.exclude.split(',');
  328. }
  329. if ( ! args.post__in ) {
  330. args.uploadedTo = attrs.id;
  331. }
  332. // Collect the attributes that were not included in `args`.
  333. others = _.omit( attrs, 'id', 'ids', 'include', 'exclude', 'orderby', 'order' );
  334. _.each( this.defaults, function( value, key ) {
  335. others[ key ] = self.coerce( others, key );
  336. });
  337. query = wp.media.query( args );
  338. query[ this.tag ] = new Backbone.Model( others );
  339. return query;
  340. },
  341. /**
  342. * Triggered when clicking 'Insert {label}' or 'Update {label}'
  343. *
  344. * @param {wp.media.model.Attachments} attachments A Backbone.Collection containing
  345. * the media items belonging to a collection.
  346. * The query[ this.tag ] property is a Backbone.Model
  347. * containing the 'props' for the collection.
  348. * @returns {wp.shortcode}
  349. */
  350. shortcode: function( attachments ) {
  351. var props = attachments.props.toJSON(),
  352. attrs = _.pick( props, 'orderby', 'order' ),
  353. shortcode, clone;
  354. if ( attachments.type ) {
  355. attrs.type = attachments.type;
  356. delete attachments.type;
  357. }
  358. if ( attachments[this.tag] ) {
  359. _.extend( attrs, attachments[this.tag].toJSON() );
  360. }
  361. // Convert all gallery shortcodes to use the `ids` property.
  362. // Ignore `post__in` and `post__not_in`; the attachments in
  363. // the collection will already reflect those properties.
  364. attrs.ids = attachments.pluck('id');
  365. // Copy the `uploadedTo` post ID.
  366. if ( props.uploadedTo ) {
  367. attrs.id = props.uploadedTo;
  368. }
  369. // Check if the gallery is randomly ordered.
  370. delete attrs.orderby;
  371. if ( attrs._orderbyRandom ) {
  372. attrs.orderby = 'rand';
  373. } else if ( attrs._orderByField && attrs._orderByField != 'rand' ) {
  374. attrs.orderby = attrs._orderByField;
  375. }
  376. delete attrs._orderbyRandom;
  377. delete attrs._orderByField;
  378. // If the `ids` attribute is set and `orderby` attribute
  379. // is the default value, clear it for cleaner output.
  380. if ( attrs.ids && 'post__in' === attrs.orderby ) {
  381. delete attrs.orderby;
  382. }
  383. attrs = this.setDefaults( attrs );
  384. shortcode = new wp.shortcode({
  385. tag: this.tag,
  386. attrs: attrs,
  387. type: 'single'
  388. });
  389. // Use a cloned version of the gallery.
  390. clone = new wp.media.model.Attachments( attachments.models, {
  391. props: props
  392. });
  393. clone[ this.tag ] = attachments[ this.tag ];
  394. collections[ shortcode.string() ] = clone;
  395. return shortcode;
  396. },
  397. /**
  398. * Triggered when double-clicking a collection shortcode placeholder
  399. * in the editor
  400. *
  401. * @param {string} content Content that is searched for possible
  402. * shortcode markup matching the passed tag name,
  403. *
  404. * @this wp.media.{prop}
  405. *
  406. * @returns {wp.media.view.MediaFrame.Select} A media workflow.
  407. */
  408. edit: function( content ) {
  409. var shortcode = wp.shortcode.next( this.tag, content ),
  410. defaultPostId = this.defaults.id,
  411. attachments, selection, state;
  412. // Bail if we didn't match the shortcode or all of the content.
  413. if ( ! shortcode || shortcode.content !== content ) {
  414. return;
  415. }
  416. // Ignore the rest of the match object.
  417. shortcode = shortcode.shortcode;
  418. if ( _.isUndefined( shortcode.get('id') ) && ! _.isUndefined( defaultPostId ) ) {
  419. shortcode.set( 'id', defaultPostId );
  420. }
  421. attachments = this.attachments( shortcode );
  422. selection = new wp.media.model.Selection( attachments.models, {
  423. props: attachments.props.toJSON(),
  424. multiple: true
  425. });
  426. selection[ this.tag ] = attachments[ this.tag ];
  427. // Fetch the query's attachments, and then break ties from the
  428. // query to allow for sorting.
  429. selection.more().done( function() {
  430. // Break ties with the query.
  431. selection.props.set({ query: false });
  432. selection.unmirror();
  433. selection.props.unset('orderby');
  434. });
  435. // Destroy the previous gallery frame.
  436. if ( this.frame ) {
  437. this.frame.dispose();
  438. }
  439. if ( shortcode.attrs.named.type && 'video' === shortcode.attrs.named.type ) {
  440. state = 'video-' + this.tag + '-edit';
  441. } else {
  442. state = this.tag + '-edit';
  443. }
  444. // Store the current frame.
  445. this.frame = wp.media({
  446. frame: 'post',
  447. state: state,
  448. title: this.editTitle,
  449. editing: true,
  450. multiple: true,
  451. selection: selection
  452. }).open();
  453. return this.frame;
  454. },
  455. setDefaults: function( attrs ) {
  456. var self = this;
  457. // Remove default attributes from the shortcode.
  458. _.each( this.defaults, function( value, key ) {
  459. attrs[ key ] = self.coerce( attrs, key );
  460. if ( value === attrs[ key ] ) {
  461. delete attrs[ key ];
  462. }
  463. });
  464. return attrs;
  465. }
  466. }, attributes );
  467. };
  468. wp.media._galleryDefaults = {
  469. itemtag: 'dl',
  470. icontag: 'dt',
  471. captiontag: 'dd',
  472. columns: '3',
  473. link: 'post',
  474. size: 'thumbnail',
  475. order: 'ASC',
  476. id: wp.media.view.settings.post && wp.media.view.settings.post.id,
  477. orderby : 'menu_order ID'
  478. };
  479. if ( wp.media.view.settings.galleryDefaults ) {
  480. wp.media.galleryDefaults = _.extend( {}, wp.media._galleryDefaults, wp.media.view.settings.galleryDefaults );
  481. } else {
  482. wp.media.galleryDefaults = wp.media._galleryDefaults;
  483. }
  484. wp.media.gallery = new wp.media.collection({
  485. tag: 'gallery',
  486. type : 'image',
  487. editTitle : wp.media.view.l10n.editGalleryTitle,
  488. defaults : wp.media.galleryDefaults,
  489. setDefaults: function( attrs ) {
  490. var self = this, changed = ! _.isEqual( wp.media.galleryDefaults, wp.media._galleryDefaults );
  491. _.each( this.defaults, function( value, key ) {
  492. attrs[ key ] = self.coerce( attrs, key );
  493. if ( value === attrs[ key ] && ( ! changed || value === wp.media._galleryDefaults[ key ] ) ) {
  494. delete attrs[ key ];
  495. }
  496. } );
  497. return attrs;
  498. }
  499. });
  500. /**
  501. * @namespace wp.media.featuredImage
  502. * @memberOf wp.media
  503. */
  504. wp.media.featuredImage = {
  505. /**
  506. * Get the featured image post ID
  507. *
  508. * @returns {wp.media.view.settings.post.featuredImageId|number}
  509. */
  510. get: function() {
  511. return wp.media.view.settings.post.featuredImageId;
  512. },
  513. /**
  514. * Set the featured image id, save the post thumbnail data and
  515. * set the HTML in the post meta box to the new featured image.
  516. *
  517. * @param {number} id The post ID of the featured image, or -1 to unset it.
  518. */
  519. set: function( id ) {
  520. var settings = wp.media.view.settings;
  521. settings.post.featuredImageId = id;
  522. wp.media.post( 'get-post-thumbnail-html', {
  523. post_id: settings.post.id,
  524. thumbnail_id: settings.post.featuredImageId,
  525. _wpnonce: settings.post.nonce
  526. }).done( function( html ) {
  527. if ( html == '0' ) {
  528. window.alert( window.setPostThumbnailL10n.error );
  529. return;
  530. }
  531. $( '.inside', '#postimagediv' ).html( html );
  532. });
  533. },
  534. /**
  535. * Remove the featured image id, save the post thumbnail data and
  536. * set the HTML in the post meta box to no featured image.
  537. */
  538. remove: function() {
  539. wp.media.featuredImage.set( -1 );
  540. },
  541. /**
  542. * The Featured Image workflow
  543. *
  544. * @this wp.media.featuredImage
  545. *
  546. * @returns {wp.media.view.MediaFrame.Select} A media workflow.
  547. */
  548. frame: function() {
  549. if ( this._frame ) {
  550. wp.media.frame = this._frame;
  551. return this._frame;
  552. }
  553. this._frame = wp.media({
  554. state: 'featured-image',
  555. states: [ new wp.media.controller.FeaturedImage() , new wp.media.controller.EditImage() ]
  556. });
  557. this._frame.on( 'toolbar:create:featured-image', function( toolbar ) {
  558. /**
  559. * @this wp.media.view.MediaFrame.Select
  560. */
  561. this.createSelectToolbar( toolbar, {
  562. text: wp.media.view.l10n.setFeaturedImage
  563. });
  564. }, this._frame );
  565. this._frame.on( 'content:render:edit-image', function() {
  566. var selection = this.state('featured-image').get('selection'),
  567. view = new wp.media.view.EditImage( { model: selection.single(), controller: this } ).render();
  568. this.content.set( view );
  569. // after bringing in the frame, load the actual editor via an ajax call
  570. view.loadEditor();
  571. }, this._frame );
  572. this._frame.state('featured-image').on( 'select', this.select );
  573. return this._frame;
  574. },
  575. /**
  576. * 'select' callback for Featured Image workflow, triggered when
  577. * the 'Set Featured Image' button is clicked in the media modal.
  578. *
  579. * @this wp.media.controller.FeaturedImage
  580. */
  581. select: function() {
  582. var selection = this.get('selection').single();
  583. if ( ! wp.media.view.settings.post.featuredImageId ) {
  584. return;
  585. }
  586. wp.media.featuredImage.set( selection ? selection.id : -1 );
  587. },
  588. /**
  589. * Open the content media manager to the 'featured image' tab when
  590. * the post thumbnail is clicked.
  591. *
  592. * Update the featured image id when the 'remove' link is clicked.
  593. */
  594. init: function() {
  595. $('#postimagediv').on( 'click', '#set-post-thumbnail', function( event ) {
  596. event.preventDefault();
  597. // Stop propagation to prevent thickbox from activating.
  598. event.stopPropagation();
  599. wp.media.featuredImage.frame().open();
  600. }).on( 'click', '#remove-post-thumbnail', function() {
  601. wp.media.featuredImage.remove();
  602. return false;
  603. });
  604. }
  605. };
  606. $( wp.media.featuredImage.init );
  607. /** @namespace wp.media.editor */
  608. wp.media.editor = {
  609. /**
  610. * Send content to the editor
  611. *
  612. * @param {string} html Content to send to the editor
  613. */
  614. insert: function( html ) {
  615. var editor, wpActiveEditor,
  616. hasTinymce = ! _.isUndefined( window.tinymce ),
  617. hasQuicktags = ! _.isUndefined( window.QTags );
  618. if ( this.activeEditor ) {
  619. wpActiveEditor = window.wpActiveEditor = this.activeEditor;
  620. } else {
  621. wpActiveEditor = window.wpActiveEditor;
  622. }
  623. // Delegate to the global `send_to_editor` if it exists.
  624. // This attempts to play nice with any themes/plugins that have
  625. // overridden the insert functionality.
  626. if ( window.send_to_editor ) {
  627. return window.send_to_editor.apply( this, arguments );
  628. }
  629. if ( ! wpActiveEditor ) {
  630. if ( hasTinymce && tinymce.activeEditor ) {
  631. editor = tinymce.activeEditor;
  632. wpActiveEditor = window.wpActiveEditor = editor.id;
  633. } else if ( ! hasQuicktags ) {
  634. return false;
  635. }
  636. } else if ( hasTinymce ) {
  637. editor = tinymce.get( wpActiveEditor );
  638. }
  639. if ( editor && ! editor.isHidden() ) {
  640. editor.execCommand( 'mceInsertContent', false, html );
  641. } else if ( hasQuicktags ) {
  642. QTags.insertContent( html );
  643. } else {
  644. document.getElementById( wpActiveEditor ).value += html;
  645. }
  646. // If the old thickbox remove function exists, call it in case
  647. // a theme/plugin overloaded it.
  648. if ( window.tb_remove ) {
  649. try { window.tb_remove(); } catch( e ) {}
  650. }
  651. },
  652. /**
  653. * Setup 'workflow' and add to the 'workflows' cache. 'open' can
  654. * subsequently be called upon it.
  655. *
  656. * @param {string} id A slug used to identify the workflow.
  657. * @param {Object} [options={}]
  658. *
  659. * @this wp.media.editor
  660. *
  661. * @returns {wp.media.view.MediaFrame.Select} A media workflow.
  662. */
  663. add: function( id, options ) {
  664. var workflow = this.get( id );
  665. // only add once: if exists return existing
  666. if ( workflow ) {
  667. return workflow;
  668. }
  669. workflow = workflows[ id ] = wp.media( _.defaults( options || {}, {
  670. frame: 'post',
  671. state: 'insert',
  672. title: wp.media.view.l10n.addMedia,
  673. multiple: true
  674. } ) );
  675. workflow.on( 'insert', function( selection ) {
  676. var state = workflow.state();
  677. selection = selection || state.get('selection');
  678. if ( ! selection )
  679. return;
  680. $.when.apply( $, selection.map( function( attachment ) {
  681. var display = state.display( attachment ).toJSON();
  682. /**
  683. * @this wp.media.editor
  684. */
  685. return this.send.attachment( display, attachment.toJSON() );
  686. }, this ) ).done( function() {
  687. wp.media.editor.insert( _.toArray( arguments ).join('\n\n') );
  688. });
  689. }, this );
  690. workflow.state('gallery-edit').on( 'update', function( selection ) {
  691. /**
  692. * @this wp.media.editor
  693. */
  694. this.insert( wp.media.gallery.shortcode( selection ).string() );
  695. }, this );
  696. workflow.state('playlist-edit').on( 'update', function( selection ) {
  697. /**
  698. * @this wp.media.editor
  699. */
  700. this.insert( wp.media.playlist.shortcode( selection ).string() );
  701. }, this );
  702. workflow.state('video-playlist-edit').on( 'update', function( selection ) {
  703. /**
  704. * @this wp.media.editor
  705. */
  706. this.insert( wp.media.playlist.shortcode( selection ).string() );
  707. }, this );
  708. workflow.state('embed').on( 'select', function() {
  709. /**
  710. * @this wp.media.editor
  711. */
  712. var state = workflow.state(),
  713. type = state.get('type'),
  714. embed = state.props.toJSON();
  715. embed.url = embed.url || '';
  716. if ( 'link' === type ) {
  717. _.defaults( embed, {
  718. linkText: embed.url,
  719. linkUrl: embed.url
  720. });
  721. this.send.link( embed ).done( function( resp ) {
  722. wp.media.editor.insert( resp );
  723. });
  724. } else if ( 'image' === type ) {
  725. _.defaults( embed, {
  726. title: embed.url,
  727. linkUrl: '',
  728. align: 'none',
  729. link: 'none'
  730. });
  731. if ( 'none' === embed.link ) {
  732. embed.linkUrl = '';
  733. } else if ( 'file' === embed.link ) {
  734. embed.linkUrl = embed.url;
  735. }
  736. this.insert( wp.media.string.image( embed ) );
  737. }
  738. }, this );
  739. workflow.state('featured-image').on( 'select', wp.media.featuredImage.select );
  740. workflow.setState( workflow.options.state );
  741. return workflow;
  742. },
  743. /**
  744. * Determines the proper current workflow id
  745. *
  746. * @param {string} [id=''] A slug used to identify the workflow.
  747. *
  748. * @returns {wpActiveEditor|string|tinymce.activeEditor.id}
  749. */
  750. id: function( id ) {
  751. if ( id ) {
  752. return id;
  753. }
  754. // If an empty `id` is provided, default to `wpActiveEditor`.
  755. id = window.wpActiveEditor;
  756. // If that doesn't work, fall back to `tinymce.activeEditor.id`.
  757. if ( ! id && ! _.isUndefined( window.tinymce ) && tinymce.activeEditor ) {
  758. id = tinymce.activeEditor.id;
  759. }
  760. // Last but not least, fall back to the empty string.
  761. id = id || '';
  762. return id;
  763. },
  764. /**
  765. * Return the workflow specified by id
  766. *
  767. * @param {string} id A slug used to identify the workflow.
  768. *
  769. * @this wp.media.editor
  770. *
  771. * @returns {wp.media.view.MediaFrame} A media workflow.
  772. */
  773. get: function( id ) {
  774. id = this.id( id );
  775. return workflows[ id ];
  776. },
  777. /**
  778. * Remove the workflow represented by id from the workflow cache
  779. *
  780. * @param {string} id A slug used to identify the workflow.
  781. *
  782. * @this wp.media.editor
  783. */
  784. remove: function( id ) {
  785. id = this.id( id );
  786. delete workflows[ id ];
  787. },
  788. /** @namespace wp.media.editor.send */
  789. send: {
  790. /**
  791. * Called when sending an attachment to the editor
  792. * from the medial modal.
  793. *
  794. * @param {Object} props Attachment details (align, link, size, etc).
  795. * @param {Object} attachment The attachment object, media version of Post.
  796. * @returns {Promise}
  797. */
  798. attachment: function( props, attachment ) {
  799. var caption = attachment.caption,
  800. options, html;
  801. // If captions are disabled, clear the caption.
  802. if ( ! wp.media.view.settings.captions ) {
  803. delete attachment.caption;
  804. }
  805. props = wp.media.string.props( props, attachment );
  806. options = {
  807. id: attachment.id,
  808. post_content: attachment.description,
  809. post_excerpt: caption
  810. };
  811. if ( props.linkUrl ) {
  812. options.url = props.linkUrl;
  813. }
  814. if ( 'image' === attachment.type ) {
  815. html = wp.media.string.image( props );
  816. _.each({
  817. align: 'align',
  818. size: 'image-size',
  819. alt: 'image_alt'
  820. }, function( option, prop ) {
  821. if ( props[ prop ] )
  822. options[ option ] = props[ prop ];
  823. });
  824. } else if ( 'video' === attachment.type ) {
  825. html = wp.media.string.video( props, attachment );
  826. } else if ( 'audio' === attachment.type ) {
  827. html = wp.media.string.audio( props, attachment );
  828. } else {
  829. html = wp.media.string.link( props );
  830. options.post_title = props.title;
  831. }
  832. return wp.media.post( 'send-attachment-to-editor', {
  833. nonce: wp.media.view.settings.nonce.sendToEditor,
  834. attachment: options,
  835. html: html,
  836. post_id: wp.media.view.settings.post.id
  837. });
  838. },
  839. /**
  840. * Called when 'Insert From URL' source is not an image. Example: YouTube url.
  841. *
  842. * @param {Object} embed
  843. * @returns {Promise}
  844. */
  845. link: function( embed ) {
  846. return wp.media.post( 'send-link-to-editor', {
  847. nonce: wp.media.view.settings.nonce.sendToEditor,
  848. src: embed.linkUrl,
  849. link_text: embed.linkText,
  850. html: wp.media.string.link( embed ),
  851. post_id: wp.media.view.settings.post.id
  852. });
  853. }
  854. },
  855. /**
  856. * Open a workflow
  857. *
  858. * @param {string} [id=undefined] Optional. A slug used to identify the workflow.
  859. * @param {Object} [options={}]
  860. *
  861. * @this wp.media.editor
  862. *
  863. * @returns {wp.media.view.MediaFrame}
  864. */
  865. open: function( id, options ) {
  866. var workflow;
  867. options = options || {};
  868. id = this.id( id );
  869. this.activeEditor = id;
  870. workflow = this.get( id );
  871. // Redo workflow if state has changed
  872. if ( ! workflow || ( workflow.options && options.state !== workflow.options.state ) ) {
  873. workflow = this.add( id, options );
  874. }
  875. wp.media.frame = workflow;
  876. return workflow.open();
  877. },
  878. /**
  879. * Bind click event for .insert-media using event delegation
  880. */
  881. init: function() {
  882. $(document.body)
  883. .on( 'click.add-media-button', '.insert-media', function( event ) {
  884. var elem = $( event.currentTarget ),
  885. editor = elem.data('editor'),
  886. options = {
  887. frame: 'post',
  888. state: 'insert',
  889. title: wp.media.view.l10n.addMedia,
  890. multiple: true
  891. };
  892. event.preventDefault();
  893. if ( elem.hasClass( 'gallery' ) ) {
  894. options.state = 'gallery';
  895. options.title = wp.media.view.l10n.createGalleryTitle;
  896. }
  897. wp.media.editor.open( editor, options );
  898. });
  899. // Initialize and render the Editor drag-and-drop uploader.
  900. new wp.media.view.EditorUploader().render();
  901. }
  902. };
  903. _.bindAll( wp.media.editor, 'open' );
  904. $( wp.media.editor.init );
  905. }(jQuery, _));