editor.js 44 KB

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