wp/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
changeset 7 cf61fcea0001
child 16 a86126ab1dd4
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     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 );