angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

plugin.js (8802B)


      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 );