You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

349 lines
8.6 KiB

  1. /**
  2. * Text pattern plugin for TinyMCE
  3. *
  4. * @since 4.3.0
  5. *
  6. * This plugin can automatically format text patterns as you type. It includes several groups of patterns.
  7. *
  8. * Start of line patterns:
  9. * As-you-type:
  10. * - Unordered list (`* ` and `- `).
  11. * - Ordered list (`1. ` and `1) `).
  12. *
  13. * On enter:
  14. * - h2 (## ).
  15. * - h3 (### ).
  16. * - h4 (#### ).
  17. * - h5 (##### ).
  18. * - h6 (###### ).
  19. * - blockquote (> ).
  20. * - hr (---).
  21. *
  22. * Inline patterns:
  23. * - <code> (`) (backtick).
  24. *
  25. * If the transformation in unwanted, the user can undo the change by pressing backspace,
  26. * using the undo shortcut, or the undo button in the toolbar.
  27. *
  28. * Setting for the patterns can be overridden by plugins by using the `tiny_mce_before_init` PHP filter.
  29. * The setting name is `wptextpattern` and the value is an object containing override arrays for each
  30. * patterns group. There are three groups: "space", "enter", and "inline". Example (PHP):
  31. *
  32. * add_filter( 'tiny_mce_before_init', 'my_mce_init_wptextpattern' );
  33. * function my_mce_init_wptextpattern( $init ) {
  34. * $init['wptextpattern'] = wp_json_encode( array(
  35. * 'inline' => array(
  36. * array( 'delimiter' => '**', 'format' => 'bold' ),
  37. * array( 'delimiter' => '__', 'format' => 'italic' ),
  38. * ),
  39. * ) );
  40. *
  41. * return $init;
  42. * }
  43. *
  44. * Note that setting this will override the default text patterns. You will need to include them
  45. * in your settings array if you want to keep them working.
  46. */
  47. ( function( tinymce, setTimeout ) {
  48. if ( tinymce.Env.ie && tinymce.Env.ie < 9 ) {
  49. return;
  50. }
  51. /**
  52. * Escapes characters for use in a Regular Expression.
  53. *
  54. * @param {String} string Characters to escape
  55. *
  56. * @return {String} Escaped characters
  57. */
  58. function escapeRegExp( string ) {
  59. return string.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' );
  60. }
  61. tinymce.PluginManager.add( 'wptextpattern', function( editor ) {
  62. var VK = tinymce.util.VK;
  63. var settings = editor.settings.wptextpattern || {};
  64. var spacePatterns = settings.space || [
  65. { regExp: /^[*-]\s/, cmd: 'InsertUnorderedList' },
  66. { regExp: /^1[.)]\s/, cmd: 'InsertOrderedList' }
  67. ];
  68. var enterPatterns = settings.enter || [
  69. { start: '##', format: 'h2' },
  70. { start: '###', format: 'h3' },
  71. { start: '####', format: 'h4' },
  72. { start: '#####', format: 'h5' },
  73. { start: '######', format: 'h6' },
  74. { start: '>', format: 'blockquote' },
  75. { regExp: /^(-){3,}$/, element: 'hr' }
  76. ];
  77. var inlinePatterns = settings.inline || [
  78. { delimiter: '`', format: 'code' }
  79. ];
  80. var canUndo;
  81. editor.on( 'selectionchange', function() {
  82. canUndo = null;
  83. } );
  84. editor.on( 'keydown', function( event ) {
  85. if ( ( canUndo && event.keyCode === 27 /* ESCAPE */ ) || ( canUndo === 'space' && event.keyCode === VK.BACKSPACE ) ) {
  86. editor.undoManager.undo();
  87. event.preventDefault();
  88. event.stopImmediatePropagation();
  89. }
  90. if ( VK.metaKeyPressed( event ) ) {
  91. return;
  92. }
  93. if ( event.keyCode === VK.ENTER ) {
  94. enter();
  95. // Wait for the browser to insert the character.
  96. } else if ( event.keyCode === VK.SPACEBAR ) {
  97. setTimeout( space );
  98. } else if ( event.keyCode > 47 && ! ( event.keyCode >= 91 && event.keyCode <= 93 ) ) {
  99. setTimeout( inline );
  100. }
  101. }, true );
  102. function inline() {
  103. var rng = editor.selection.getRng();
  104. var node = rng.startContainer;
  105. var offset = rng.startOffset;
  106. var startOffset;
  107. var endOffset;
  108. var pattern;
  109. var format;
  110. var zero;
  111. // We need a non empty text node with an offset greater than zero.
  112. if ( ! node || node.nodeType !== 3 || ! node.data.length || ! offset ) {
  113. return;
  114. }
  115. var string = node.data.slice( 0, offset );
  116. var lastChar = node.data.charAt( offset - 1 );
  117. tinymce.each( inlinePatterns, function( p ) {
  118. // Character before selection should be delimiter.
  119. if ( lastChar !== p.delimiter.slice( -1 ) ) {
  120. return;
  121. }
  122. var escDelimiter = escapeRegExp( p.delimiter );
  123. var delimiterFirstChar = p.delimiter.charAt( 0 );
  124. var regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' );
  125. var match = string.match( regExp );
  126. if ( ! match ) {
  127. return;
  128. }
  129. startOffset = match[1].length;
  130. endOffset = offset - p.delimiter.length;
  131. var before = string.charAt( startOffset - 1 );
  132. var after = string.charAt( startOffset + p.delimiter.length );
  133. // test*test* => format applied
  134. // test *test* => applied
  135. // test* test* => not applied
  136. if ( startOffset && /\S/.test( before ) ) {
  137. if ( /\s/.test( after ) || before === delimiterFirstChar ) {
  138. return;
  139. }
  140. }
  141. // Do not replace when only whitespace and delimiter characters.
  142. if ( ( new RegExp( '^[\\s' + escapeRegExp( delimiterFirstChar ) + ']+$' ) ).test( string.slice( startOffset, endOffset ) ) ) {
  143. return;
  144. }
  145. pattern = p;
  146. return false;
  147. } );
  148. if ( ! pattern ) {
  149. return;
  150. }
  151. format = editor.formatter.get( pattern.format );
  152. if ( format && format[0].inline ) {
  153. editor.undoManager.add();
  154. editor.undoManager.transact( function() {
  155. node.insertData( offset, '\uFEFF' );
  156. node = node.splitText( startOffset );
  157. zero = node.splitText( offset - startOffset );
  158. node.deleteData( 0, pattern.delimiter.length );
  159. node.deleteData( node.data.length - pattern.delimiter.length, pattern.delimiter.length );
  160. editor.formatter.apply( pattern.format, {}, node );
  161. editor.selection.setCursorLocation( zero, 1 );
  162. } );
  163. // We need to wait for native events to be triggered.
  164. setTimeout( function() {
  165. canUndo = 'space';
  166. editor.once( 'selectionchange', function() {
  167. var offset;
  168. if ( zero ) {
  169. offset = zero.data.indexOf( '\uFEFF' );
  170. if ( offset !== -1 ) {
  171. zero.deleteData( offset, offset + 1 );
  172. }
  173. }
  174. } );
  175. } );
  176. }
  177. }
  178. function firstTextNode( node ) {
  179. var parent = editor.dom.getParent( node, 'p' ),
  180. child;
  181. if ( ! parent ) {
  182. return;
  183. }
  184. while ( child = parent.firstChild ) {
  185. if ( child.nodeType !== 3 ) {
  186. parent = child;
  187. } else {
  188. break;
  189. }
  190. }
  191. if ( ! child ) {
  192. return;
  193. }
  194. if ( ! child.data ) {
  195. if ( child.nextSibling && child.nextSibling.nodeType === 3 ) {
  196. child = child.nextSibling;
  197. } else {
  198. child = null;
  199. }
  200. }
  201. return child;
  202. }
  203. function space() {
  204. var rng = editor.selection.getRng(),
  205. node = rng.startContainer,
  206. parent,
  207. text;
  208. if ( ! node || firstTextNode( node ) !== node ) {
  209. return;
  210. }
  211. parent = node.parentNode;
  212. text = node.data;
  213. tinymce.each( spacePatterns, function( pattern ) {
  214. var match = text.match( pattern.regExp );
  215. if ( ! match || rng.startOffset !== match[0].length ) {
  216. return;
  217. }
  218. editor.undoManager.add();
  219. editor.undoManager.transact( function() {
  220. node.deleteData( 0, match[0].length );
  221. if ( ! parent.innerHTML ) {
  222. parent.appendChild( document.createElement( 'br' ) );
  223. }
  224. editor.selection.setCursorLocation( parent );
  225. editor.execCommand( pattern.cmd );
  226. } );
  227. // We need to wait for native events to be triggered.
  228. setTimeout( function() {
  229. canUndo = 'space';
  230. } );
  231. return false;
  232. } );
  233. }
  234. function enter() {
  235. var rng = editor.selection.getRng(),
  236. start = rng.startContainer,
  237. node = firstTextNode( start ),
  238. i = enterPatterns.length,
  239. text, pattern, parent;
  240. if ( ! node ) {
  241. return;
  242. }
  243. text = node.data;
  244. while ( i-- ) {
  245. if ( enterPatterns[ i ].start ) {
  246. if ( text.indexOf( enterPatterns[ i ].start ) === 0 ) {
  247. pattern = enterPatterns[ i ];
  248. break;
  249. }
  250. } else if ( enterPatterns[ i ].regExp ) {
  251. if ( enterPatterns[ i ].regExp.test( text ) ) {
  252. pattern = enterPatterns[ i ];
  253. break;
  254. }
  255. }
  256. }
  257. if ( ! pattern ) {
  258. return;
  259. }
  260. if ( node === start && tinymce.trim( text ) === pattern.start ) {
  261. return;
  262. }
  263. editor.once( 'keyup', function() {
  264. editor.undoManager.add();
  265. editor.undoManager.transact( function() {
  266. if ( pattern.format ) {
  267. editor.formatter.apply( pattern.format, {}, node );
  268. node.replaceData( 0, node.data.length, ltrim( node.data.slice( pattern.start.length ) ) );
  269. } else if ( pattern.element ) {
  270. parent = node.parentNode && node.parentNode.parentNode;
  271. if ( parent ) {
  272. parent.replaceChild( document.createElement( pattern.element ), node.parentNode );
  273. }
  274. }
  275. } );
  276. // We need to wait for native events to be triggered.
  277. setTimeout( function() {
  278. canUndo = 'enter';
  279. } );
  280. } );
  281. }
  282. function ltrim( text ) {
  283. return text ? text.replace( /^\s+/, '' ) : '';
  284. }
  285. } );
  286. } )( window.tinymce, window.setTimeout );