]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
WordPress 4.7.2-scripts
[autoinstalls/wordpress.git] / wp-includes / js / tinymce / plugins / wptextpattern / plugin.js
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 );