|
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 /** |
|
53 * Escapes characters for use in a Regular Expression. |
|
54 * |
|
55 * @param {String} string Characters to escape |
|
56 * |
|
57 * @return {String} Escaped characters |
|
58 */ |
|
59 function escapeRegExp( string ) { |
|
60 return string.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ); |
|
61 } |
|
62 |
|
63 tinymce.PluginManager.add( 'wptextpattern', function( editor ) { |
|
64 var VK = tinymce.util.VK; |
|
65 var settings = editor.settings.wptextpattern || {}; |
|
66 |
|
67 var spacePatterns = settings.space || [ |
|
68 { regExp: /^[*-]\s/, cmd: 'InsertUnorderedList' }, |
|
69 { regExp: /^1[.)]\s/, cmd: 'InsertOrderedList' } |
|
70 ]; |
|
71 |
|
72 var enterPatterns = settings.enter || [ |
|
73 { start: '##', format: 'h2' }, |
|
74 { start: '###', format: 'h3' }, |
|
75 { start: '####', format: 'h4' }, |
|
76 { start: '#####', format: 'h5' }, |
|
77 { start: '######', format: 'h6' }, |
|
78 { start: '>', format: 'blockquote' }, |
|
79 { regExp: /^(-){3,}$/, element: 'hr' } |
|
80 ]; |
|
81 |
|
82 var inlinePatterns = settings.inline || [ |
|
83 { delimiter: '`', format: 'code' } |
|
84 ]; |
|
85 |
|
86 var canUndo; |
|
87 |
|
88 editor.on( 'selectionchange', function() { |
|
89 canUndo = null; |
|
90 } ); |
|
91 |
|
92 editor.on( 'keydown', function( event ) { |
|
93 if ( ( canUndo && event.keyCode === 27 /* ESCAPE */ ) || ( canUndo === 'space' && event.keyCode === VK.BACKSPACE ) ) { |
|
94 editor.undoManager.undo(); |
|
95 event.preventDefault(); |
|
96 event.stopImmediatePropagation(); |
|
97 } |
|
98 |
|
99 if ( VK.metaKeyPressed( event ) ) { |
|
100 return; |
|
101 } |
|
102 |
|
103 if ( event.keyCode === VK.ENTER ) { |
|
104 enter(); |
|
105 // Wait for the browser to insert the character. |
|
106 } else if ( event.keyCode === VK.SPACEBAR ) { |
|
107 setTimeout( space ); |
|
108 } else if ( event.keyCode > 47 && ! ( event.keyCode >= 91 && event.keyCode <= 93 ) ) { |
|
109 setTimeout( inline ); |
|
110 } |
|
111 }, true ); |
|
112 |
|
113 function inline() { |
|
114 var rng = editor.selection.getRng(); |
|
115 var node = rng.startContainer; |
|
116 var offset = rng.startOffset; |
|
117 var startOffset; |
|
118 var endOffset; |
|
119 var pattern; |
|
120 var format; |
|
121 var zero; |
|
122 |
|
123 // We need a non empty text node with an offset greater than zero. |
|
124 if ( ! node || node.nodeType !== 3 || ! node.data.length || ! offset ) { |
|
125 return; |
|
126 } |
|
127 |
|
128 var string = node.data.slice( 0, offset ); |
|
129 var lastChar = node.data.charAt( offset - 1 ); |
|
130 |
|
131 tinymce.each( inlinePatterns, function( p ) { |
|
132 // Character before selection should be delimiter. |
|
133 if ( lastChar !== p.delimiter.slice( -1 ) ) { |
|
134 return; |
|
135 } |
|
136 |
|
137 var escDelimiter = escapeRegExp( p.delimiter ); |
|
138 var delimiterFirstChar = p.delimiter.charAt( 0 ); |
|
139 var regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' ); |
|
140 var match = string.match( regExp ); |
|
141 |
|
142 if ( ! match ) { |
|
143 return; |
|
144 } |
|
145 |
|
146 startOffset = match[1].length; |
|
147 endOffset = offset - p.delimiter.length; |
|
148 |
|
149 var before = string.charAt( startOffset - 1 ); |
|
150 var after = string.charAt( startOffset + p.delimiter.length ); |
|
151 |
|
152 // test*test* => format applied |
|
153 // test *test* => applied |
|
154 // test* test* => not applied |
|
155 if ( startOffset && /\S/.test( before ) ) { |
|
156 if ( /\s/.test( after ) || before === delimiterFirstChar ) { |
|
157 return; |
|
158 } |
|
159 } |
|
160 |
|
161 // Do not replace when only whitespace and delimiter characters. |
|
162 if ( ( new RegExp( '^[\\s' + escapeRegExp( delimiterFirstChar ) + ']+$' ) ).test( string.slice( startOffset, endOffset ) ) ) { |
|
163 return; |
|
164 } |
|
165 |
|
166 pattern = p; |
|
167 |
|
168 return false; |
|
169 } ); |
|
170 |
|
171 if ( ! pattern ) { |
|
172 return; |
|
173 } |
|
174 |
|
175 format = editor.formatter.get( pattern.format ); |
|
176 |
|
177 if ( format && format[0].inline ) { |
|
178 editor.undoManager.add(); |
|
179 |
|
180 editor.undoManager.transact( function() { |
|
181 node.insertData( offset, '\uFEFF' ); |
|
182 |
|
183 node = node.splitText( startOffset ); |
|
184 zero = node.splitText( offset - startOffset ); |
|
185 |
|
186 node.deleteData( 0, pattern.delimiter.length ); |
|
187 node.deleteData( node.data.length - pattern.delimiter.length, pattern.delimiter.length ); |
|
188 |
|
189 editor.formatter.apply( pattern.format, {}, node ); |
|
190 |
|
191 editor.selection.setCursorLocation( zero, 1 ); |
|
192 } ); |
|
193 |
|
194 // We need to wait for native events to be triggered. |
|
195 setTimeout( function() { |
|
196 canUndo = 'space'; |
|
197 |
|
198 editor.once( 'selectionchange', function() { |
|
199 var offset; |
|
200 |
|
201 if ( zero ) { |
|
202 offset = zero.data.indexOf( '\uFEFF' ); |
|
203 |
|
204 if ( offset !== -1 ) { |
|
205 zero.deleteData( offset, offset + 1 ); |
|
206 } |
|
207 } |
|
208 } ); |
|
209 } ); |
|
210 } |
|
211 } |
|
212 |
|
213 function firstTextNode( node ) { |
|
214 var parent = editor.dom.getParent( node, 'p' ), |
|
215 child; |
|
216 |
|
217 if ( ! parent ) { |
|
218 return; |
|
219 } |
|
220 |
|
221 while ( child = parent.firstChild ) { |
|
222 if ( child.nodeType !== 3 ) { |
|
223 parent = child; |
|
224 } else { |
|
225 break; |
|
226 } |
|
227 } |
|
228 |
|
229 if ( ! child ) { |
|
230 return; |
|
231 } |
|
232 |
|
233 if ( ! child.data ) { |
|
234 if ( child.nextSibling && child.nextSibling.nodeType === 3 ) { |
|
235 child = child.nextSibling; |
|
236 } else { |
|
237 child = null; |
|
238 } |
|
239 } |
|
240 |
|
241 return child; |
|
242 } |
|
243 |
|
244 function space() { |
|
245 var rng = editor.selection.getRng(), |
|
246 node = rng.startContainer, |
|
247 parent, |
|
248 text; |
|
249 |
|
250 if ( ! node || firstTextNode( node ) !== node ) { |
|
251 return; |
|
252 } |
|
253 |
|
254 parent = node.parentNode; |
|
255 text = node.data; |
|
256 |
|
257 tinymce.each( spacePatterns, function( pattern ) { |
|
258 var match = text.match( pattern.regExp ); |
|
259 |
|
260 if ( ! match || rng.startOffset !== match[0].length ) { |
|
261 return; |
|
262 } |
|
263 |
|
264 editor.undoManager.add(); |
|
265 |
|
266 editor.undoManager.transact( function() { |
|
267 node.deleteData( 0, match[0].length ); |
|
268 |
|
269 if ( ! parent.innerHTML ) { |
|
270 parent.appendChild( document.createElement( 'br' ) ); |
|
271 } |
|
272 |
|
273 editor.selection.setCursorLocation( parent ); |
|
274 editor.execCommand( pattern.cmd ); |
|
275 } ); |
|
276 |
|
277 // We need to wait for native events to be triggered. |
|
278 setTimeout( function() { |
|
279 canUndo = 'space'; |
|
280 } ); |
|
281 |
|
282 return false; |
|
283 } ); |
|
284 } |
|
285 |
|
286 function enter() { |
|
287 var rng = editor.selection.getRng(), |
|
288 start = rng.startContainer, |
|
289 node = firstTextNode( start ), |
|
290 i = enterPatterns.length, |
|
291 text, pattern, parent; |
|
292 |
|
293 if ( ! node ) { |
|
294 return; |
|
295 } |
|
296 |
|
297 text = node.data; |
|
298 |
|
299 while ( i-- ) { |
|
300 if ( enterPatterns[ i ].start ) { |
|
301 if ( text.indexOf( enterPatterns[ i ].start ) === 0 ) { |
|
302 pattern = enterPatterns[ i ]; |
|
303 break; |
|
304 } |
|
305 } else if ( enterPatterns[ i ].regExp ) { |
|
306 if ( enterPatterns[ i ].regExp.test( text ) ) { |
|
307 pattern = enterPatterns[ i ]; |
|
308 break; |
|
309 } |
|
310 } |
|
311 } |
|
312 |
|
313 if ( ! pattern ) { |
|
314 return; |
|
315 } |
|
316 |
|
317 if ( node === start && tinymce.trim( text ) === pattern.start ) { |
|
318 return; |
|
319 } |
|
320 |
|
321 editor.once( 'keyup', function() { |
|
322 editor.undoManager.add(); |
|
323 |
|
324 editor.undoManager.transact( function() { |
|
325 if ( pattern.format ) { |
|
326 editor.formatter.apply( pattern.format, {}, node ); |
|
327 node.replaceData( 0, node.data.length, ltrim( node.data.slice( pattern.start.length ) ) ); |
|
328 } else if ( pattern.element ) { |
|
329 parent = node.parentNode && node.parentNode.parentNode; |
|
330 |
|
331 if ( parent ) { |
|
332 parent.replaceChild( document.createElement( pattern.element ), node.parentNode ); |
|
333 } |
|
334 } |
|
335 } ); |
|
336 |
|
337 // We need to wait for native events to be triggered. |
|
338 setTimeout( function() { |
|
339 canUndo = 'enter'; |
|
340 } ); |
|
341 } ); |
|
342 } |
|
343 |
|
344 function ltrim( text ) { |
|
345 return text ? text.replace( /^\s+/, '' ) : ''; |
|
346 } |
|
347 } ); |
|
348 } )( window.tinymce, window.setTimeout ); |