editor.js 44 KB

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