editor.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414
  1. /**
  2. * @output wp-admin/js/editor.js
  3. */
  4. window.wp = window.wp || {};
  5. ( function( $, wp ) {
  6. wp.editor = wp.editor || {};
  7. /**
  8. * Utility functions for the editor.
  9. *
  10. * @since 2.5.0
  11. */
  12. function SwitchEditors() {
  13. var tinymce, $$,
  14. exports = {};
  15. function init() {
  16. if ( ! tinymce && window.tinymce ) {
  17. tinymce = window.tinymce;
  18. $$ = tinymce.$;
  19. /**
  20. * Handles onclick events for the Visual/Text tabs.
  21. *
  22. * @since 4.3.0
  23. *
  24. * @returns {void}
  25. */
  26. $$( document ).on( 'click', function( event ) {
  27. var id, mode,
  28. target = $$( event.target );
  29. if ( target.hasClass( 'wp-switch-editor' ) ) {
  30. id = target.attr( 'data-wp-editor-id' );
  31. mode = target.hasClass( 'switch-tmce' ) ? 'tmce' : 'html';
  32. switchEditor( id, mode );
  33. }
  34. });
  35. }
  36. }
  37. /**
  38. * Returns the height of the editor toolbar(s) in px.
  39. *
  40. * @since 3.9.0
  41. *
  42. * @param {Object} editor The TinyMCE editor.
  43. * @returns {number} If the height is between 10 and 200 return the height,
  44. * else return 30.
  45. */
  46. function getToolbarHeight( editor ) {
  47. var node = $$( '.mce-toolbar-grp', editor.getContainer() )[0],
  48. height = node && node.clientHeight;
  49. if ( height && height > 10 && height < 200 ) {
  50. return parseInt( height, 10 );
  51. }
  52. return 30;
  53. }
  54. /**
  55. * Switches the editor between Visual and Text mode.
  56. *
  57. * @since 2.5.0
  58. *
  59. * @memberof switchEditors
  60. *
  61. * @param {string} id The id of the editor you want to change the editor mode for. Default: `content`.
  62. * @param {string} mode The mode you want to switch to. Default: `toggle`.
  63. * @returns {void}
  64. */
  65. function switchEditor( id, mode ) {
  66. id = id || 'content';
  67. mode = mode || 'toggle';
  68. var editorHeight, toolbarHeight, iframe,
  69. editor = tinymce.get( id ),
  70. wrap = $$( '#wp-' + id + '-wrap' ),
  71. $textarea = $$( '#' + id ),
  72. textarea = $textarea[0];
  73. if ( 'toggle' === mode ) {
  74. if ( editor && ! editor.isHidden() ) {
  75. mode = 'html';
  76. } else {
  77. mode = 'tmce';
  78. }
  79. }
  80. if ( 'tmce' === mode || 'tinymce' === mode ) {
  81. // If the editor is visible we are already in `tinymce` mode.
  82. if ( editor && ! editor.isHidden() ) {
  83. return false;
  84. }
  85. // Insert closing tags for any open tags in QuickTags.
  86. if ( typeof( window.QTags ) !== 'undefined' ) {
  87. window.QTags.closeAllTags( id );
  88. }
  89. editorHeight = parseInt( textarea.style.height, 10 ) || 0;
  90. var keepSelection = false;
  91. if ( editor ) {
  92. keepSelection = editor.getParam( 'wp_keep_scroll_position' );
  93. } else {
  94. keepSelection = window.tinyMCEPreInit.mceInit[ id ] &&
  95. window.tinyMCEPreInit.mceInit[ id ].wp_keep_scroll_position;
  96. }
  97. if ( keepSelection ) {
  98. // Save the selection
  99. addHTMLBookmarkInTextAreaContent( $textarea );
  100. }
  101. if ( editor ) {
  102. editor.show();
  103. // No point to resize the iframe in iOS.
  104. if ( ! tinymce.Env.iOS && editorHeight ) {
  105. toolbarHeight = getToolbarHeight( editor );
  106. editorHeight = editorHeight - toolbarHeight + 14;
  107. // Sane limit for the editor height.
  108. if ( editorHeight > 50 && editorHeight < 5000 ) {
  109. editor.theme.resizeTo( null, editorHeight );
  110. }
  111. }
  112. if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
  113. // Restore the selection
  114. focusHTMLBookmarkInVisualEditor( editor );
  115. }
  116. } else {
  117. tinymce.init( window.tinyMCEPreInit.mceInit[ id ] );
  118. }
  119. wrap.removeClass( 'html-active' ).addClass( 'tmce-active' );
  120. $textarea.attr( 'aria-hidden', true );
  121. window.setUserSetting( 'editor', 'tinymce' );
  122. } else if ( 'html' === mode ) {
  123. // If the editor is hidden (Quicktags is shown) we don't need to switch.
  124. if ( editor && editor.isHidden() ) {
  125. return false;
  126. }
  127. if ( editor ) {
  128. // Don't resize the textarea in iOS. The iframe is forced to 100% height there, we shouldn't match it.
  129. if ( ! tinymce.Env.iOS ) {
  130. iframe = editor.iframeElement;
  131. editorHeight = iframe ? parseInt( iframe.style.height, 10 ) : 0;
  132. if ( editorHeight ) {
  133. toolbarHeight = getToolbarHeight( editor );
  134. editorHeight = editorHeight + toolbarHeight - 14;
  135. // Sane limit for the textarea height.
  136. if ( editorHeight > 50 && editorHeight < 5000 ) {
  137. textarea.style.height = editorHeight + 'px';
  138. }
  139. }
  140. }
  141. var selectionRange = null;
  142. if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
  143. selectionRange = findBookmarkedPosition( editor );
  144. }
  145. editor.hide();
  146. if ( selectionRange ) {
  147. selectTextInTextArea( editor, selectionRange );
  148. }
  149. } else {
  150. // There is probably a JS error on the page. The TinyMCE editor instance doesn't exist. Show the textarea.
  151. $textarea.css({ 'display': '', 'visibility': '' });
  152. }
  153. wrap.removeClass( 'tmce-active' ).addClass( 'html-active' );
  154. $textarea.attr( 'aria-hidden', false );
  155. window.setUserSetting( 'editor', 'html' );
  156. }
  157. }
  158. /**
  159. * Checks if a cursor is inside an HTML tag or comment.
  160. *
  161. * In order to prevent breaking HTML tags when selecting text, the cursor
  162. * must be moved to either the start or end of the tag.
  163. *
  164. * This will prevent the selection marker to be inserted in the middle of an HTML tag.
  165. *
  166. * This function gives information whether the cursor is inside a tag or not, as well as
  167. * the tag type, if it is a closing tag and check if the HTML tag is inside a shortcode tag,
  168. * e.g. `[caption]<img.../>..`.
  169. *
  170. * @param {string} content The test content where the cursor is.
  171. * @param {number} cursorPosition The cursor position inside the content.
  172. *
  173. * @returns {(null|Object)} Null if cursor is not in a tag, Object if the cursor is inside a tag.
  174. */
  175. function getContainingTagInfo( content, cursorPosition ) {
  176. var lastLtPos = content.lastIndexOf( '<', cursorPosition - 1 ),
  177. lastGtPos = content.lastIndexOf( '>', cursorPosition );
  178. if ( lastLtPos > lastGtPos || content.substr( cursorPosition, 1 ) === '>' ) {
  179. // find what the tag is
  180. var tagContent = content.substr( lastLtPos ),
  181. tagMatch = tagContent.match( /<\s*(\/)?(\w+|\!-{2}.*-{2})/ );
  182. if ( ! tagMatch ) {
  183. return null;
  184. }
  185. var tagType = tagMatch[2],
  186. closingGt = tagContent.indexOf( '>' );
  187. return {
  188. ltPos: lastLtPos,
  189. gtPos: lastLtPos + closingGt + 1, // offset by one to get the position _after_ the character,
  190. tagType: tagType,
  191. isClosingTag: !! tagMatch[1]
  192. };
  193. }
  194. return null;
  195. }
  196. /**
  197. * Checks if the cursor is inside a shortcode
  198. *
  199. * If the cursor is inside a shortcode wrapping tag, e.g. `[caption]` it's better to
  200. * move the selection marker to before or after the shortcode.
  201. *
  202. * For example `[caption]` rewrites/removes anything that's between the `[caption]` tag and the
  203. * `<img/>` tag inside.
  204. *
  205. * `[caption]<span>ThisIsGone</span><img .../>[caption]`
  206. *
  207. * Moving the selection to before or after the short code is better, since it allows to select
  208. * something, instead of just losing focus and going to the start of the content.
  209. *
  210. * @param {string} content The text content to check against.
  211. * @param {number} cursorPosition The cursor position to check.
  212. *
  213. * @return {(undefined|Object)} Undefined if the cursor is not wrapped in a shortcode tag.
  214. * Information about the wrapping shortcode tag if it's wrapped in one.
  215. */
  216. function getShortcodeWrapperInfo( content, cursorPosition ) {
  217. var contentShortcodes = getShortCodePositionsInText( content );
  218. for ( var i = 0; i < contentShortcodes.length; i++ ) {
  219. var element = contentShortcodes[ i ];
  220. if ( cursorPosition >= element.startIndex && cursorPosition <= element.endIndex ) {
  221. return element;
  222. }
  223. }
  224. }
  225. /**
  226. * Gets a list of unique shortcodes or shortcode-look-alikes in the content.
  227. *
  228. * @param {string} content The content we want to scan for shortcodes.
  229. */
  230. function getShortcodesInText( content ) {
  231. var shortcodes = content.match( /\[+([\w_-])+/g ),
  232. result = [];
  233. if ( shortcodes ) {
  234. for ( var i = 0; i < shortcodes.length; i++ ) {
  235. var shortcode = shortcodes[ i ].replace( /^\[+/g, '' );
  236. if ( result.indexOf( shortcode ) === -1 ) {
  237. result.push( shortcode );
  238. }
  239. }
  240. }
  241. return result;
  242. }
  243. /**
  244. * Gets all shortcodes and their positions in the content
  245. *
  246. * This function returns all the shortcodes that could be found in the textarea content
  247. * along with their character positions and boundaries.
  248. *
  249. * This is used to check if the selection cursor is inside the boundaries of a shortcode
  250. * and move it accordingly, to avoid breakage.
  251. *
  252. * @link adjustTextAreaSelectionCursors
  253. *
  254. * The information can also be used in other cases when we need to lookup shortcode data,
  255. * as it's already structured!
  256. *
  257. * @param {string} content The content we want to scan for shortcodes
  258. */
  259. function getShortCodePositionsInText( content ) {
  260. var allShortcodes = getShortcodesInText( content ), shortcodeInfo;
  261. if ( allShortcodes.length === 0 ) {
  262. return [];
  263. }
  264. var shortcodeDetailsRegexp = wp.shortcode.regexp( allShortcodes.join( '|' ) ),
  265. shortcodeMatch, // Define local scope for the variable to be used in the loop below.
  266. shortcodesDetails = [];
  267. while ( shortcodeMatch = shortcodeDetailsRegexp.exec( content ) ) {
  268. /**
  269. * Check if the shortcode should be shown as plain text.
  270. *
  271. * This corresponds to the [[shortcode]] syntax, which doesn't parse the shortcode
  272. * and just shows it as text.
  273. */
  274. var showAsPlainText = shortcodeMatch[1] === '[';
  275. shortcodeInfo = {
  276. shortcodeName: shortcodeMatch[2],
  277. showAsPlainText: showAsPlainText,
  278. startIndex: shortcodeMatch.index,
  279. endIndex: shortcodeMatch.index + shortcodeMatch[0].length,
  280. length: shortcodeMatch[0].length
  281. };
  282. shortcodesDetails.push( shortcodeInfo );
  283. }
  284. /**
  285. * Get all URL matches, and treat them as embeds.
  286. *
  287. * Since there isn't a good way to detect if a URL by itself on a line is a previewable
  288. * object, it's best to treat all of them as such.
  289. *
  290. * This means that the selection will capture the whole URL, in a similar way shrotcodes
  291. * are treated.
  292. */
  293. var urlRegexp = new RegExp(
  294. '(^|[\\n\\r][\\n\\r]|<p>)(https?:\\/\\/[^\s"]+?)(<\\/p>\s*|[\\n\\r][\\n\\r]|$)', 'gi'
  295. );
  296. while ( shortcodeMatch = urlRegexp.exec( content ) ) {
  297. shortcodeInfo = {
  298. shortcodeName: 'url',
  299. showAsPlainText: false,
  300. startIndex: shortcodeMatch.index,
  301. endIndex: shortcodeMatch.index + shortcodeMatch[ 0 ].length,
  302. length: shortcodeMatch[ 0 ].length,
  303. urlAtStartOfContent: shortcodeMatch[ 1 ] === '',
  304. urlAtEndOfContent: shortcodeMatch[ 3 ] === ''
  305. };
  306. shortcodesDetails.push( shortcodeInfo );
  307. }
  308. return shortcodesDetails;
  309. }
  310. /**
  311. * Generate a cursor marker element to be inserted in the content.
  312. *
  313. * `span` seems to be the least destructive element that can be used.
  314. *
  315. * Using DomQuery syntax to create it, since it's used as both text and as a DOM element.
  316. *
  317. * @param {Object} domLib DOM library instance.
  318. * @param {string} content The content to insert into the cusror marker element.
  319. */
  320. function getCursorMarkerSpan( domLib, content ) {
  321. return domLib( '<span>' ).css( {
  322. display: 'inline-block',
  323. width: 0,
  324. overflow: 'hidden',
  325. 'line-height': 0
  326. } )
  327. .html( content ? content : '' );
  328. }
  329. /**
  330. * Gets adjusted selection cursor positions according to HTML tags, comments, and shortcodes.
  331. *
  332. * Shortcodes and HTML codes are a bit of a special case when selecting, since they may render
  333. * content in Visual mode. If we insert selection markers somewhere inside them, it's really possible
  334. * to break the syntax and render the HTML tag or shortcode broken.
  335. *
  336. * @link getShortcodeWrapperInfo
  337. *
  338. * @param {string} content Textarea content that the cursors are in
  339. * @param {{cursorStart: number, cursorEnd: number}} cursorPositions Cursor start and end positions
  340. *
  341. * @return {{cursorStart: number, cursorEnd: number}}
  342. */
  343. function adjustTextAreaSelectionCursors( content, cursorPositions ) {
  344. var voidElements = [
  345. 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
  346. 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
  347. ];
  348. var cursorStart = cursorPositions.cursorStart,
  349. cursorEnd = cursorPositions.cursorEnd,
  350. // check if the cursor is in a tag and if so, adjust it
  351. isCursorStartInTag = getContainingTagInfo( content, cursorStart );
  352. if ( isCursorStartInTag ) {
  353. /**
  354. * Only move to the start of the HTML tag (to select the whole element) if the tag
  355. * is part of the voidElements list above.
  356. *
  357. * This list includes tags that are self-contained and don't need a closing tag, according to the
  358. * HTML5 specification.
  359. *
  360. * This is done in order to make selection of text a bit more consistent when selecting text in
  361. * `<p>` tags or such.
  362. *
  363. * In cases where the tag is not a void element, the cursor is put to the end of the tag,
  364. * so it's either between the opening and closing tag elements or after the closing tag.
  365. */
  366. if ( voidElements.indexOf( isCursorStartInTag.tagType ) !== -1 ) {
  367. cursorStart = isCursorStartInTag.ltPos;
  368. } else {
  369. cursorStart = isCursorStartInTag.gtPos;
  370. }
  371. }
  372. var isCursorEndInTag = getContainingTagInfo( content, cursorEnd );
  373. if ( isCursorEndInTag ) {
  374. cursorEnd = isCursorEndInTag.gtPos;
  375. }
  376. var isCursorStartInShortcode = getShortcodeWrapperInfo( content, cursorStart );
  377. if ( isCursorStartInShortcode && ! isCursorStartInShortcode.showAsPlainText ) {
  378. /**
  379. * If a URL is at the start or the end of the content,
  380. * the selection doesn't work, because it inserts a marker in the text,
  381. * which breaks the embedURL detection.
  382. *
  383. * The best way to avoid that and not modify the user content is to
  384. * adjust the cursor to either after or before URL.
  385. */
  386. if ( isCursorStartInShortcode.urlAtStartOfContent ) {
  387. cursorStart = isCursorStartInShortcode.endIndex;
  388. } else {
  389. cursorStart = isCursorStartInShortcode.startIndex;
  390. }
  391. }
  392. var isCursorEndInShortcode = getShortcodeWrapperInfo( content, cursorEnd );
  393. if ( isCursorEndInShortcode && ! isCursorEndInShortcode.showAsPlainText ) {
  394. if ( isCursorEndInShortcode.urlAtEndOfContent ) {
  395. cursorEnd = isCursorEndInShortcode.startIndex;
  396. } else {
  397. cursorEnd = isCursorEndInShortcode.endIndex;
  398. }
  399. }
  400. return {
  401. cursorStart: cursorStart,
  402. cursorEnd: cursorEnd
  403. };
  404. }
  405. /**
  406. * Adds text selection markers in the editor textarea.
  407. *
  408. * Adds selection markers in the content of the editor `textarea`.
  409. * The method directly manipulates the `textarea` content, to allow TinyMCE plugins
  410. * to run after the markers are added.
  411. *
  412. * @param {object} $textarea TinyMCE's textarea wrapped as a DomQuery object
  413. */
  414. function addHTMLBookmarkInTextAreaContent( $textarea ) {
  415. if ( ! $textarea || ! $textarea.length ) {
  416. // If no valid $textarea object is provided, there's nothing we can do.
  417. return;
  418. }
  419. var textArea = $textarea[0],
  420. textAreaContent = textArea.value,
  421. adjustedCursorPositions = adjustTextAreaSelectionCursors( textAreaContent, {
  422. cursorStart: textArea.selectionStart,
  423. cursorEnd: textArea.selectionEnd
  424. } ),
  425. htmlModeCursorStartPosition = adjustedCursorPositions.cursorStart,
  426. htmlModeCursorEndPosition = adjustedCursorPositions.cursorEnd,
  427. mode = htmlModeCursorStartPosition !== htmlModeCursorEndPosition ? 'range' : 'single',
  428. selectedText = null,
  429. cursorMarkerSkeleton = getCursorMarkerSpan( $$, '&#65279;' ).attr( 'data-mce-type','bookmark' );
  430. if ( mode === 'range' ) {
  431. var markedText = textArea.value.slice( htmlModeCursorStartPosition, htmlModeCursorEndPosition ),
  432. bookMarkEnd = cursorMarkerSkeleton.clone().addClass( 'mce_SELRES_end' );
  433. selectedText = [
  434. markedText,
  435. bookMarkEnd[0].outerHTML
  436. ].join( '' );
  437. }
  438. textArea.value = [
  439. textArea.value.slice( 0, htmlModeCursorStartPosition ), // text until the cursor/selection position
  440. cursorMarkerSkeleton.clone() // cursor/selection start marker
  441. .addClass( 'mce_SELRES_start' )[0].outerHTML,
  442. selectedText, // selected text with end cursor/position marker
  443. textArea.value.slice( htmlModeCursorEndPosition ) // text from last cursor/selection position to end
  444. ].join( '' );
  445. }
  446. /**
  447. * Focuses the selection markers in Visual mode.
  448. *
  449. * The method checks for existing selection markers inside the editor DOM (Visual mode)
  450. * and create a selection between the two nodes using the DOM `createRange` selection API
  451. *
  452. * If there is only a single node, select only the single node through TinyMCE's selection API
  453. *
  454. * @param {Object} editor TinyMCE editor instance.
  455. */
  456. function focusHTMLBookmarkInVisualEditor( editor ) {
  457. var startNode = editor.$( '.mce_SELRES_start' ).attr( 'data-mce-bogus', 1 ),
  458. endNode = editor.$( '.mce_SELRES_end' ).attr( 'data-mce-bogus', 1 );
  459. if ( startNode.length ) {
  460. editor.focus();
  461. if ( ! endNode.length ) {
  462. editor.selection.select( startNode[0] );
  463. } else {
  464. var selection = editor.getDoc().createRange();
  465. selection.setStartAfter( startNode[0] );
  466. selection.setEndBefore( endNode[0] );
  467. editor.selection.setRng( selection );
  468. }
  469. }
  470. if ( editor.getParam( 'wp_keep_scroll_position' ) ) {
  471. scrollVisualModeToStartElement( editor, startNode );
  472. }
  473. removeSelectionMarker( startNode );
  474. removeSelectionMarker( endNode );
  475. editor.save();
  476. }
  477. /**
  478. * Removes selection marker and the parent node if it is an empty paragraph.
  479. *
  480. * By default TinyMCE wraps loose inline tags in a `<p>`.
  481. * When removing selection markers an empty `<p>` may be left behind, remove it.
  482. *
  483. * @param {object} $marker The marker to be removed from the editor DOM, wrapped in an instnce of `editor.$`
  484. */
  485. function removeSelectionMarker( $marker ) {
  486. var $markerParent = $marker.parent();
  487. $marker.remove();
  488. //Remove empty paragraph left over after removing the marker.
  489. if ( $markerParent.is( 'p' ) && ! $markerParent.children().length && ! $markerParent.text() ) {
  490. $markerParent.remove();
  491. }
  492. }
  493. /**
  494. * Scrolls the content to place the selected element in the center of the screen.
  495. *
  496. * Takes an element, that is usually the selection start element, selected in
  497. * `focusHTMLBookmarkInVisualEditor()` and scrolls the screen so the element appears roughly
  498. * in the middle of the screen.
  499. *
  500. * I order to achieve the proper positioning, the editor media bar and toolbar are subtracted
  501. * from the window height, to get the proper viewport window, that the user sees.
  502. *
  503. * @param {Object} editor TinyMCE editor instance.
  504. * @param {Object} element HTMLElement that should be scrolled into view.
  505. */
  506. function scrollVisualModeToStartElement( editor, element ) {
  507. var elementTop = editor.$( element ).offset().top,
  508. TinyMCEContentAreaTop = editor.$( editor.getContentAreaContainer() ).offset().top,
  509. toolbarHeight = getToolbarHeight( editor ),
  510. edTools = $( '#wp-content-editor-tools' ),
  511. edToolsHeight = 0,
  512. edToolsOffsetTop = 0,
  513. $scrollArea;
  514. if ( edTools.length ) {
  515. edToolsHeight = edTools.height();
  516. edToolsOffsetTop = edTools.offset().top;
  517. }
  518. var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
  519. selectionPosition = TinyMCEContentAreaTop + elementTop,
  520. visibleAreaHeight = windowHeight - ( edToolsHeight + toolbarHeight );
  521. // There's no need to scroll if the selection is inside the visible area.
  522. if ( selectionPosition < visibleAreaHeight ) {
  523. return;
  524. }
  525. /**
  526. * The minimum scroll height should be to the top of the editor, to offer a consistent
  527. * experience.
  528. *
  529. * In order to find the top of the editor, we calculate the offset of `#wp-content-editor-tools` and
  530. * subtracting the height. This gives the scroll position where the top of the editor tools aligns with
  531. * the top of the viewport (under the Master Bar)
  532. */
  533. var adjustedScroll;
  534. if ( editor.settings.wp_autoresize_on ) {
  535. $scrollArea = $( 'html,body' );
  536. adjustedScroll = Math.max( selectionPosition - visibleAreaHeight / 2, edToolsOffsetTop - edToolsHeight );
  537. } else {
  538. $scrollArea = $( editor.contentDocument ).find( 'html,body' );
  539. adjustedScroll = elementTop;
  540. }
  541. $scrollArea.animate( {
  542. scrollTop: parseInt( adjustedScroll, 10 )
  543. }, 100 );
  544. }
  545. /**
  546. * This method was extracted from the `SaveContent` hook in
  547. * `wp-includes/js/tinymce/plugins/wordpress/plugin.js`.
  548. *
  549. * It's needed here, since the method changes the content a bit, which confuses the cursor position.
  550. *
  551. * @param {Object} event TinyMCE event object.
  552. */
  553. function fixTextAreaContent( event ) {
  554. // Keep empty paragraphs :(
  555. event.content = event.content.replace( /<p>(?:<br ?\/?>|\u00a0|\uFEFF| )*<\/p>/g, '<p>&nbsp;</p>' );
  556. }
  557. /**
  558. * Finds the current selection position in the Visual editor.
  559. *
  560. * Find the current selection in the Visual editor by inserting marker elements at the start
  561. * and end of the selection.
  562. *
  563. * Uses the standard DOM selection API to achieve that goal.
  564. *
  565. * Check the notes in the comments in the code below for more information on some gotchas
  566. * and why this solution was chosen.
  567. *
  568. * @param {Object} editor The editor where we must find the selection
  569. * @returns {(null|Object)} The selection range position in the editor
  570. */
  571. function findBookmarkedPosition( editor ) {
  572. // Get the TinyMCE `window` reference, since we need to access the raw selection.
  573. var TinyMCEWindow = editor.getWin(),
  574. selection = TinyMCEWindow.getSelection();
  575. if ( ! selection || selection.rangeCount < 1 ) {
  576. // no selection, no need to continue.
  577. return;
  578. }
  579. /**
  580. * The ID is used to avoid replacing user generated content, that may coincide with the
  581. * format specified below.
  582. * @type {string}
  583. */
  584. var selectionID = 'SELRES_' + Math.random();
  585. /**
  586. * Create two marker elements that will be used to mark the start and the end of the range.
  587. *
  588. * The elements have hardcoded style that makes them invisible. This is done to avoid seeing
  589. * random content flickering in the editor when switching between modes.
  590. */
  591. var spanSkeleton = getCursorMarkerSpan( editor.$, selectionID ),
  592. startElement = spanSkeleton.clone().addClass( 'mce_SELRES_start' ),
  593. endElement = spanSkeleton.clone().addClass( 'mce_SELRES_end' );
  594. /**
  595. * Inspired by:
  596. * @link https://stackoverflow.com/a/17497803/153310
  597. *
  598. * Why do it this way and not with TinyMCE's bookmarks?
  599. *
  600. * TinyMCE's bookmarks are very nice when working with selections and positions, BUT
  601. * there is no way to determine the precise position of the bookmark when switching modes, since
  602. * TinyMCE does some serialization of the content, to fix things like shortcodes, run plugins, prettify
  603. * HTML code and so on. In this process, the bookmark markup gets lost.
  604. *
  605. * If we decide to hook right after the bookmark is added, we can see where the bookmark is in the raw HTML
  606. * in TinyMCE. Unfortunately this state is before the serialization, so any visual markup in the content will
  607. * throw off the positioning.
  608. *
  609. * To avoid this, we insert two custom `span`s that will serve as the markers at the beginning and end of the
  610. * selection.
  611. *
  612. * Why not use TinyMCE's selection API or the DOM API to wrap the contents? Because if we do that, this creates
  613. * a new node, which is inserted in the dom. Now this will be fine, if we worked with fixed selections to
  614. * full nodes. Unfortunately in our case, the user can select whatever they like, which means that the
  615. * selection may start in the middle of one node and end in the middle of a completely different one. If we
  616. * wrap the selection in another node, this will create artifacts in the content.
  617. *
  618. * Using the method below, we insert the custom `span` nodes at the start and at the end of the selection.
  619. * This helps us not break the content and also gives us the option to work with multi-node selections without
  620. * breaking the markup.
  621. */
  622. var range = selection.getRangeAt( 0 ),
  623. startNode = range.startContainer,
  624. startOffset = range.startOffset,
  625. boundaryRange = range.cloneRange();
  626. /**
  627. * If the selection is on a shortcode with Live View, TinyMCE creates a bogus markup,
  628. * which we have to account for.
  629. */
  630. if ( editor.$( startNode ).parents( '.mce-offscreen-selection' ).length > 0 ) {
  631. startNode = editor.$( '[data-mce-selected]' )[0];
  632. /**
  633. * Marking the start and end element with `data-mce-object-selection` helps
  634. * discern when the selected object is a Live Preview selection.
  635. *
  636. * This way we can adjust the selection to properly select only the content, ignoring
  637. * whitespace inserted around the selected object by the Editor.
  638. */
  639. startElement.attr( 'data-mce-object-selection', 'true' );
  640. endElement.attr( 'data-mce-object-selection', 'true' );
  641. editor.$( startNode ).before( startElement[0] );
  642. editor.$( startNode ).after( endElement[0] );
  643. } else {
  644. boundaryRange.collapse( false );
  645. boundaryRange.insertNode( endElement[0] );
  646. boundaryRange.setStart( startNode, startOffset );
  647. boundaryRange.collapse( true );
  648. boundaryRange.insertNode( startElement[0] );
  649. range.setStartAfter( startElement[0] );
  650. range.setEndBefore( endElement[0] );
  651. selection.removeAllRanges();
  652. selection.addRange( range );
  653. }
  654. /**
  655. * Now the editor's content has the start/end nodes.
  656. *
  657. * Unfortunately the content goes through some more changes after this step, before it gets inserted
  658. * in the `textarea`. This means that we have to do some minor cleanup on our own here.
  659. */
  660. editor.on( 'GetContent', fixTextAreaContent );
  661. var content = removep( editor.getContent() );
  662. editor.off( 'GetContent', fixTextAreaContent );
  663. startElement.remove();
  664. endElement.remove();
  665. var startRegex = new RegExp(
  666. '<span[^>]*\\s*class="mce_SELRES_start"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>(\\s*)'
  667. );
  668. var endRegex = new RegExp(
  669. '(\\s*)<span[^>]*\\s*class="mce_SELRES_end"[^>]+>\\s*' + selectionID + '[^<]*<\\/span>'
  670. );
  671. var startMatch = content.match( startRegex ),
  672. endMatch = content.match( endRegex );
  673. if ( ! startMatch ) {
  674. return null;
  675. }
  676. var startIndex = startMatch.index,
  677. startMatchLength = startMatch[0].length,
  678. endIndex = null;
  679. if (endMatch) {
  680. /**
  681. * Adjust the selection index, if the selection contains a Live Preview object or not.
  682. *
  683. * Check where the `data-mce-object-selection` attribute is set above for more context.
  684. */
  685. if ( startMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
  686. startMatchLength -= startMatch[1].length;
  687. }
  688. var endMatchIndex = endMatch.index;
  689. if ( endMatch[0].indexOf( 'data-mce-object-selection' ) !== -1 ) {
  690. endMatchIndex -= endMatch[1].length;
  691. }
  692. // We need to adjust the end position to discard the length of the range start marker
  693. endIndex = endMatchIndex - startMatchLength;
  694. }
  695. return {
  696. start: startIndex,
  697. end: endIndex
  698. };
  699. }
  700. /**
  701. * Selects text in the TinyMCE `textarea`.
  702. *
  703. * Selects the text in TinyMCE's textarea that's between `selection.start` and `selection.end`.
  704. *
  705. * For `selection` parameter:
  706. * @link findBookmarkedPosition
  707. *
  708. * @param {Object} editor TinyMCE's editor instance.
  709. * @param {Object} selection Selection data.
  710. */
  711. function selectTextInTextArea( editor, selection ) {
  712. // only valid in the text area mode and if we have selection
  713. if ( ! selection ) {
  714. return;
  715. }
  716. var textArea = editor.getElement(),
  717. start = selection.start,
  718. end = selection.end || selection.start;
  719. if ( textArea.focus ) {
  720. // Wait for the Visual editor to be hidden, then focus and scroll to the position
  721. setTimeout( function() {
  722. textArea.setSelectionRange( start, end );
  723. if ( textArea.blur ) {
  724. // defocus before focusing
  725. textArea.blur();
  726. }
  727. textArea.focus();
  728. }, 100 );
  729. }
  730. }
  731. // Restore the selection when the editor is initialized. Needed when the Text editor is the default.
  732. $( document ).on( 'tinymce-editor-init.keep-scroll-position', function( event, editor ) {
  733. if ( editor.$( '.mce_SELRES_start' ).length ) {
  734. focusHTMLBookmarkInVisualEditor( editor );
  735. }
  736. } );
  737. /**
  738. * Replaces <p> tags with two line breaks. "Opposite" of wpautop().
  739. *
  740. * Replaces <p> tags with two line breaks except where the <p> has attributes.
  741. * Unifies whitespace.
  742. * Indents <li>, <dt> and <dd> for better readability.
  743. *
  744. * @since 2.5.0
  745. *
  746. * @memberof switchEditors
  747. *
  748. * @param {string} html The content from the editor.
  749. * @return {string} The content with stripped paragraph tags.
  750. */
  751. function removep( html ) {
  752. var blocklist = 'blockquote|ul|ol|li|dl|dt|dd|table|thead|tbody|tfoot|tr|th|td|h[1-6]|fieldset|figure',
  753. blocklist1 = blocklist + '|div|p',
  754. blocklist2 = blocklist + '|pre',
  755. preserve_linebreaks = false,
  756. preserve_br = false,
  757. preserve = [];
  758. if ( ! html ) {
  759. return '';
  760. }
  761. // Protect script and style tags.
  762. if ( html.indexOf( '<script' ) !== -1 || html.indexOf( '<style' ) !== -1 ) {
  763. html = html.replace( /<(script|style)[^>]*>[\s\S]*?<\/\1>/g, function( match ) {
  764. preserve.push( match );
  765. return '<wp-preserve>';
  766. } );
  767. }
  768. // Protect pre tags.
  769. if ( html.indexOf( '<pre' ) !== -1 ) {
  770. preserve_linebreaks = true;
  771. html = html.replace( /<pre[^>]*>[\s\S]+?<\/pre>/g, function( a ) {
  772. a = a.replace( /<br ?\/?>(\r\n|\n)?/g, '<wp-line-break>' );
  773. a = a.replace( /<\/?p( [^>]*)?>(\r\n|\n)?/g, '<wp-line-break>' );
  774. return a.replace( /\r?\n/g, '<wp-line-break>' );
  775. });
  776. }
  777. // Remove line breaks but keep <br> tags inside image captions.
  778. if ( html.indexOf( '[caption' ) !== -1 ) {
  779. preserve_br = true;
  780. html = html.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
  781. return a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' ).replace( /[\r\n\t]+/, '' );
  782. });
  783. }
  784. // Normalize white space characters before and after block tags.
  785. html = html.replace( new RegExp( '\\s*</(' + blocklist1 + ')>\\s*', 'g' ), '</$1>\n' );
  786. html = html.replace( new RegExp( '\\s*<((?:' + blocklist1 + ')(?: [^>]*)?)>', 'g' ), '\n<$1>' );
  787. // Mark </p> if it has any attributes.
  788. html = html.replace( /(<p [^>]+>.*?)<\/p>/g, '$1</p#>' );
  789. // Preserve the first <p> inside a <div>.
  790. html = html.replace( /<div( [^>]*)?>\s*<p>/gi, '<div$1>\n\n' );
  791. // Remove paragraph tags.
  792. html = html.replace( /\s*<p>/gi, '' );
  793. html = html.replace( /\s*<\/p>\s*/gi, '\n\n' );
  794. // Normalize white space chars and remove multiple line breaks.
  795. html = html.replace( /\n[\s\u00a0]+\n/g, '\n\n' );
  796. // Replace <br> tags with line breaks.
  797. html = html.replace( /(\s*)<br ?\/?>\s*/gi, function( match, space ) {
  798. if ( space && space.indexOf( '\n' ) !== -1 ) {
  799. return '\n\n';
  800. }
  801. return '\n';
  802. });
  803. // Fix line breaks around <div>.
  804. html = html.replace( /\s*<div/g, '\n<div' );
  805. html = html.replace( /<\/div>\s*/g, '</div>\n' );
  806. // Fix line breaks around caption shortcodes.
  807. html = html.replace( /\s*\[caption([^\[]+)\[\/caption\]\s*/gi, '\n\n[caption$1[/caption]\n\n' );
  808. html = html.replace( /caption\]\n\n+\[caption/g, 'caption]\n\n[caption' );
  809. // Pad block elements tags with a line break.
  810. html = html.replace( new RegExp('\\s*<((?:' + blocklist2 + ')(?: [^>]*)?)\\s*>', 'g' ), '\n<$1>' );
  811. html = html.replace( new RegExp('\\s*</(' + blocklist2 + ')>\\s*', 'g' ), '</$1>\n' );
  812. // Indent <li>, <dt> and <dd> tags.
  813. html = html.replace( /<((li|dt|dd)[^>]*)>/g, ' \t<$1>' );
  814. // Fix line breaks around <select> and <option>.
  815. if ( html.indexOf( '<option' ) !== -1 ) {
  816. html = html.replace( /\s*<option/g, '\n<option' );
  817. html = html.replace( /\s*<\/select>/g, '\n</select>' );
  818. }
  819. // Pad <hr> with two line breaks.
  820. if ( html.indexOf( '<hr' ) !== -1 ) {
  821. html = html.replace( /\s*<hr( [^>]*)?>\s*/g, '\n\n<hr$1>\n\n' );
  822. }
  823. // Remove line breaks in <object> tags.
  824. if ( html.indexOf( '<object' ) !== -1 ) {
  825. html = html.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
  826. return a.replace( /[\r\n]+/g, '' );
  827. });
  828. }
  829. // Unmark special paragraph closing tags.
  830. html = html.replace( /<\/p#>/g, '</p>\n' );
  831. // Pad remaining <p> tags whit a line break.
  832. html = html.replace( /\s*(<p [^>]+>[\s\S]*?<\/p>)/g, '\n$1' );
  833. // Trim.
  834. html = html.replace( /^\s+/, '' );
  835. html = html.replace( /[\s\u00a0]+$/, '' );
  836. if ( preserve_linebreaks ) {
  837. html = html.replace( /<wp-line-break>/g, '\n' );
  838. }
  839. if ( preserve_br ) {
  840. html = html.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
  841. }
  842. // Restore preserved tags.
  843. if ( preserve.length ) {
  844. html = html.replace( /<wp-preserve>/g, function() {
  845. return preserve.shift();
  846. } );
  847. }
  848. return html;
  849. }
  850. /**
  851. * Replaces two line breaks with a paragraph tag and one line break with a <br>.
  852. *
  853. * Similar to `wpautop()` in formatting.php.
  854. *
  855. * @since 2.5.0
  856. *
  857. * @memberof switchEditors
  858. *
  859. * @param {string} text The text input.
  860. * @returns {string} The formatted text.
  861. */
  862. function autop( text ) {
  863. var preserve_linebreaks = false,
  864. preserve_br = false,
  865. blocklist = 'table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre' +
  866. '|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section' +
  867. '|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary';
  868. // Normalize line breaks.
  869. text = text.replace( /\r\n|\r/g, '\n' );
  870. // Remove line breaks from <object>.
  871. if ( text.indexOf( '<object' ) !== -1 ) {
  872. text = text.replace( /<object[\s\S]+?<\/object>/g, function( a ) {
  873. return a.replace( /\n+/g, '' );
  874. });
  875. }
  876. // Remove line breaks from tags.
  877. text = text.replace( /<[^<>]+>/g, function( a ) {
  878. return a.replace( /[\n\t ]+/g, ' ' );
  879. });
  880. // Preserve line breaks in <pre> and <script> tags.
  881. if ( text.indexOf( '<pre' ) !== -1 || text.indexOf( '<script' ) !== -1 ) {
  882. preserve_linebreaks = true;
  883. text = text.replace( /<(pre|script)[^>]*>[\s\S]*?<\/\1>/g, function( a ) {
  884. return a.replace( /\n/g, '<wp-line-break>' );
  885. });
  886. }
  887. if ( text.indexOf( '<figcaption' ) !== -1 ) {
  888. text = text.replace( /\s*(<figcaption[^>]*>)/g, '$1' );
  889. text = text.replace( /<\/figcaption>\s*/g, '</figcaption>' );
  890. }
  891. // Keep <br> tags inside captions.
  892. if ( text.indexOf( '[caption' ) !== -1 ) {
  893. preserve_br = true;
  894. text = text.replace( /\[caption[\s\S]+?\[\/caption\]/g, function( a ) {
  895. a = a.replace( /<br([^>]*)>/g, '<wp-temp-br$1>' );
  896. a = a.replace( /<[^<>]+>/g, function( b ) {
  897. return b.replace( /[\n\t ]+/, ' ' );
  898. });
  899. return a.replace( /\s*\n\s*/g, '<wp-temp-br />' );
  900. });
  901. }
  902. text = text + '\n\n';
  903. text = text.replace( /<br \/>\s*<br \/>/gi, '\n\n' );
  904. // Pad block tags with two line breaks.
  905. text = text.replace( new RegExp( '(<(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '\n\n$1' );
  906. text = text.replace( new RegExp( '(</(?:' + blocklist + ')>)', 'gi' ), '$1\n\n' );
  907. text = text.replace( /<hr( [^>]*)?>/gi, '<hr$1>\n\n' );
  908. // Remove white space chars around <option>.
  909. text = text.replace( /\s*<option/gi, '<option' );
  910. text = text.replace( /<\/option>\s*/gi, '</option>' );
  911. // Normalize multiple line breaks and white space chars.
  912. text = text.replace( /\n\s*\n+/g, '\n\n' );
  913. // Convert two line breaks to a paragraph.
  914. text = text.replace( /([\s\S]+?)\n\n/g, '<p>$1</p>\n' );
  915. // Remove empty paragraphs.
  916. text = text.replace( /<p>\s*?<\/p>/gi, '');
  917. // Remove <p> tags that are around block tags.
  918. text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
  919. text = text.replace( /<p>(<li.+?)<\/p>/gi, '$1');
  920. // Fix <p> in blockquotes.
  921. text = text.replace( /<p>\s*<blockquote([^>]*)>/gi, '<blockquote$1><p>');
  922. text = text.replace( /<\/blockquote>\s*<\/p>/gi, '</p></blockquote>');
  923. // Remove <p> tags that are wrapped around block tags.
  924. text = text.replace( new RegExp( '<p>\\s*(</?(?:' + blocklist + ')(?: [^>]*)?>)', 'gi' ), '$1' );
  925. text = text.replace( new RegExp( '(</?(?:' + blocklist + ')(?: [^>]*)?>)\\s*</p>', 'gi' ), '$1' );
  926. text = text.replace( /(<br[^>]*>)\s*\n/gi, '$1' );
  927. // Add <br> tags.
  928. text = text.replace( /\s*\n/g, '<br />\n');
  929. // Remove <br> tags that are around block tags.
  930. text = text.replace( new RegExp( '(</?(?:' + blocklist + ')[^>]*>)\\s*<br />', 'gi' ), '$1' );
  931. text = text.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, '$1' );
  932. // Remove <p> and <br> around captions.
  933. text = text.replace( /(?:<p>|<br ?\/?>)*\s*\[caption([^\[]+)\[\/caption\]\s*(?:<\/p>|<br ?\/?>)*/gi, '[caption$1[/caption]' );
  934. // Make sure there is <p> when there is </p> inside block tags that can contain other blocks.
  935. text = text.replace( /(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function( a, b, c ) {
  936. if ( c.match( /<p( [^>]*)?>/ ) ) {
  937. return a;
  938. }
  939. return b + '<p>' + c + '</p>';
  940. });
  941. // Restore the line breaks in <pre> and <script> tags.
  942. if ( preserve_linebreaks ) {
  943. text = text.replace( /<wp-line-break>/g, '\n' );
  944. }
  945. // Restore the <br> tags in captions.
  946. if ( preserve_br ) {
  947. text = text.replace( /<wp-temp-br([^>]*)>/g, '<br$1>' );
  948. }
  949. return text;
  950. }
  951. /**
  952. * Fires custom jQuery events `beforePreWpautop` and `afterPreWpautop` when jQuery is available.
  953. *
  954. * @since 2.9.0
  955. *
  956. * @memberof switchEditors
  957. *
  958. * @param {String} html The content from the visual editor.
  959. * @returns {String} the filtered content.
  960. */
  961. function pre_wpautop( html ) {
  962. var obj = { o: exports, data: html, unfiltered: html };
  963. if ( $ ) {
  964. $( 'body' ).trigger( 'beforePreWpautop', [ obj ] );
  965. }
  966. obj.data = removep( obj.data );
  967. if ( $ ) {
  968. $( 'body' ).trigger( 'afterPreWpautop', [ obj ] );
  969. }
  970. return obj.data;
  971. }
  972. /**
  973. * Fires custom jQuery events `beforeWpautop` and `afterWpautop` when jQuery is available.
  974. *
  975. * @since 2.9.0
  976. *
  977. * @memberof switchEditors
  978. *
  979. * @param {String} text The content from the text editor.
  980. * @returns {String} filtered content.
  981. */
  982. function wpautop( text ) {
  983. var obj = { o: exports, data: text, unfiltered: text };
  984. if ( $ ) {
  985. $( 'body' ).trigger( 'beforeWpautop', [ obj ] );
  986. }
  987. obj.data = autop( obj.data );
  988. if ( $ ) {
  989. $( 'body' ).trigger( 'afterWpautop', [ obj ] );
  990. }
  991. return obj.data;
  992. }
  993. if ( $ ) {
  994. $( document ).ready( init );
  995. } else if ( document.addEventListener ) {
  996. document.addEventListener( 'DOMContentLoaded', init, false );
  997. window.addEventListener( 'load', init, false );
  998. } else if ( window.attachEvent ) {
  999. window.attachEvent( 'onload', init );
  1000. document.attachEvent( 'onreadystatechange', function() {
  1001. if ( 'complete' === document.readyState ) {
  1002. init();
  1003. }
  1004. } );
  1005. }
  1006. wp.editor.autop = wpautop;
  1007. wp.editor.removep = pre_wpautop;
  1008. exports = {
  1009. go: switchEditor,
  1010. wpautop: wpautop,
  1011. pre_wpautop: pre_wpautop,
  1012. _wp_Autop: autop,
  1013. _wp_Nop: removep
  1014. };
  1015. return exports;
  1016. }
  1017. /**
  1018. * Expose the switch editors to be used globally.
  1019. *
  1020. * @namespace switchEditors
  1021. */
  1022. window.switchEditors = new SwitchEditors();
  1023. /**
  1024. * Initialize TinyMCE and/or Quicktags. For use with wp_enqueue_editor() (PHP).
  1025. *
  1026. * Intended for use with an existing textarea that will become the Text editor tab.
  1027. * The editor width will be the width of the textarea container, height will be adjustable.
  1028. *
  1029. * Settings for both TinyMCE and Quicktags can be passed on initialization, and are "filtered"
  1030. * with custom jQuery events on the document element, wp-before-tinymce-init and wp-before-quicktags-init.
  1031. *
  1032. * @since 4.8.0
  1033. *
  1034. * @param {string} id The HTML id of the textarea that is used for the editor.
  1035. * Has to be jQuery compliant. No brackets, special chars, etc.
  1036. * @param {object} settings Example:
  1037. * settings = {
  1038. * // See https://www.tinymce.com/docs/configure/integration-and-setup/.
  1039. * // Alternatively set to `true` to use the defaults.
  1040. * tinymce: {
  1041. * setup: function( editor ) {
  1042. * console.log( 'Editor initialized', editor );
  1043. * }
  1044. * }
  1045. *
  1046. * // Alternatively set to `true` to use the defaults.
  1047. * quicktags: {
  1048. * buttons: 'strong,em,link'
  1049. * }
  1050. * }
  1051. */
  1052. wp.editor.initialize = function( id, settings ) {
  1053. var init;
  1054. var defaults;
  1055. if ( ! $ || ! id || ! wp.editor.getDefaultSettings ) {
  1056. return;
  1057. }
  1058. defaults = wp.editor.getDefaultSettings();
  1059. // Initialize TinyMCE by default
  1060. if ( ! settings ) {
  1061. settings = {
  1062. tinymce: true
  1063. };
  1064. }
  1065. // Add wrap and the Visual|Text tabs.
  1066. if ( settings.tinymce && settings.quicktags ) {
  1067. var $textarea = $( '#' + id );
  1068. var $wrap = $( '<div>' ).attr( {
  1069. 'class': 'wp-core-ui wp-editor-wrap tmce-active',
  1070. id: 'wp-' + id + '-wrap'
  1071. } );
  1072. var $editorContainer = $( '<div class="wp-editor-container">' );
  1073. var $button = $( '<button>' ).attr( {
  1074. type: 'button',
  1075. 'data-wp-editor-id': id
  1076. } );
  1077. var $editorTools = $( '<div class="wp-editor-tools">' );
  1078. if ( settings.mediaButtons ) {
  1079. var buttonText = 'Add Media';
  1080. if ( window._wpMediaViewsL10n && window._wpMediaViewsL10n.addMedia ) {
  1081. buttonText = window._wpMediaViewsL10n.addMedia;
  1082. }
  1083. var $addMediaButton = $( '<button type="button" class="button insert-media add_media">' );
  1084. $addMediaButton.append( '<span class="wp-media-buttons-icon"></span>' );
  1085. $addMediaButton.append( document.createTextNode( ' ' + buttonText ) );
  1086. $addMediaButton.data( 'editor', id );
  1087. $editorTools.append(
  1088. $( '<div class="wp-media-buttons">' )
  1089. .append( $addMediaButton )
  1090. );
  1091. }
  1092. $wrap.append(
  1093. $editorTools
  1094. .append( $( '<div class="wp-editor-tabs">' )
  1095. .append( $button.clone().attr({
  1096. id: id + '-tmce',
  1097. 'class': 'wp-switch-editor switch-tmce'
  1098. }).text( window.tinymce.translate( 'Visual' ) ) )
  1099. .append( $button.attr({
  1100. id: id + '-html',
  1101. 'class': 'wp-switch-editor switch-html'
  1102. }).text( window.tinymce.translate( 'Text' ) ) )
  1103. ).append( $editorContainer )
  1104. );
  1105. $textarea.after( $wrap );
  1106. $editorContainer.append( $textarea );
  1107. }
  1108. if ( window.tinymce && settings.tinymce ) {
  1109. if ( typeof settings.tinymce !== 'object' ) {
  1110. settings.tinymce = {};
  1111. }
  1112. init = $.extend( {}, defaults.tinymce, settings.tinymce );
  1113. init.selector = '#' + id;
  1114. $( document ).trigger( 'wp-before-tinymce-init', init );
  1115. window.tinymce.init( init );
  1116. if ( ! window.wpActiveEditor ) {
  1117. window.wpActiveEditor = id;
  1118. }
  1119. }
  1120. if ( window.quicktags && settings.quicktags ) {
  1121. if ( typeof settings.quicktags !== 'object' ) {
  1122. settings.quicktags = {};
  1123. }
  1124. init = $.extend( {}, defaults.quicktags, settings.quicktags );
  1125. init.id = id;
  1126. $( document ).trigger( 'wp-before-quicktags-init', init );
  1127. window.quicktags( init );
  1128. if ( ! window.wpActiveEditor ) {
  1129. window.wpActiveEditor = init.id;
  1130. }
  1131. }
  1132. };
  1133. /**
  1134. * Remove one editor instance.
  1135. *
  1136. * Intended for use with editors that were initialized with wp.editor.initialize().
  1137. *
  1138. * @since 4.8.0
  1139. *
  1140. * @param {string} id The HTML id of the editor textarea.
  1141. */
  1142. wp.editor.remove = function( id ) {
  1143. var mceInstance, qtInstance,
  1144. $wrap = $( '#wp-' + id + '-wrap' );
  1145. if ( window.tinymce ) {
  1146. mceInstance = window.tinymce.get( id );
  1147. if ( mceInstance ) {
  1148. if ( ! mceInstance.isHidden() ) {
  1149. mceInstance.save();
  1150. }
  1151. mceInstance.remove();
  1152. }
  1153. }
  1154. if ( window.quicktags ) {
  1155. qtInstance = window.QTags.getInstance( id );
  1156. if ( qtInstance ) {
  1157. qtInstance.remove();
  1158. }
  1159. }
  1160. if ( $wrap.length ) {
  1161. $wrap.after( $( '#' + id ) );
  1162. $wrap.remove();
  1163. }
  1164. };
  1165. /**
  1166. * Get the editor content.
  1167. *
  1168. * Intended for use with editors that were initialized with wp.editor.initialize().
  1169. *
  1170. * @since 4.8.0
  1171. *
  1172. * @param {string} id The HTML id of the editor textarea.
  1173. * @return The editor content.
  1174. */
  1175. wp.editor.getContent = function( id ) {
  1176. var editor;
  1177. if ( ! $ || ! id ) {
  1178. return;
  1179. }
  1180. if ( window.tinymce ) {
  1181. editor = window.tinymce.get( id );
  1182. if ( editor && ! editor.isHidden() ) {
  1183. editor.save();
  1184. }
  1185. }
  1186. return $( '#' + id ).val();
  1187. };
  1188. }( window.jQuery, window.wp ));