Spaces:
Sleeping
Sleeping
/** | |
* Text pattern plugin for TinyMCE | |
* | |
* @since 4.3.0 | |
* | |
* This plugin can automatically format text patterns as you type. It includes several groups of patterns. | |
* | |
* Start of line patterns: | |
* As-you-type: | |
* - Unordered list (`* ` and `- `). | |
* - Ordered list (`1. ` and `1) `). | |
* | |
* On enter: | |
* - h2 (## ). | |
* - h3 (### ). | |
* - h4 (#### ). | |
* - h5 (##### ). | |
* - h6 (###### ). | |
* - blockquote (> ). | |
* - hr (---). | |
* | |
* Inline patterns: | |
* - <code> (`) (backtick). | |
* | |
* If the transformation in unwanted, the user can undo the change by pressing backspace, | |
* using the undo shortcut, or the undo button in the toolbar. | |
* | |
* Setting for the patterns can be overridden by plugins by using the `tiny_mce_before_init` PHP filter. | |
* The setting name is `wptextpattern` and the value is an object containing override arrays for each | |
* patterns group. There are three groups: "space", "enter", and "inline". Example (PHP): | |
* | |
* add_filter( 'tiny_mce_before_init', 'my_mce_init_wptextpattern' ); | |
* function my_mce_init_wptextpattern( $init ) { | |
* $init['wptextpattern'] = wp_json_encode( array( | |
* 'inline' => array( | |
* array( 'delimiter' => '**', 'format' => 'bold' ), | |
* array( 'delimiter' => '__', 'format' => 'italic' ), | |
* ), | |
* ) ); | |
* | |
* return $init; | |
* } | |
* | |
* Note that setting this will override the default text patterns. You will need to include them | |
* in your settings array if you want to keep them working. | |
*/ | |
( function( tinymce, setTimeout ) { | |
if ( tinymce.Env.ie && tinymce.Env.ie < 9 ) { | |
return; | |
} | |
/** | |
* Escapes characters for use in a Regular Expression. | |
* | |
* @param {String} string Characters to escape | |
* | |
* @return {String} Escaped characters | |
*/ | |
function escapeRegExp( string ) { | |
return string.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ); | |
} | |
tinymce.PluginManager.add( 'wptextpattern', function( editor ) { | |
var VK = tinymce.util.VK; | |
var settings = editor.settings.wptextpattern || {}; | |
var spacePatterns = settings.space || [ | |
{ regExp: /^[*-]\s/, cmd: 'InsertUnorderedList' }, | |
{ regExp: /^1[.)]\s/, cmd: 'InsertOrderedList' } | |
]; | |
var enterPatterns = settings.enter || [ | |
{ start: '##', format: 'h2' }, | |
{ start: '###', format: 'h3' }, | |
{ start: '####', format: 'h4' }, | |
{ start: '#####', format: 'h5' }, | |
{ start: '######', format: 'h6' }, | |
{ start: '>', format: 'blockquote' }, | |
{ regExp: /^(-){3,}$/, element: 'hr' } | |
]; | |
var inlinePatterns = settings.inline || [ | |
{ delimiter: '`', format: 'code' } | |
]; | |
var canUndo; | |
editor.on( 'selectionchange', function() { | |
canUndo = null; | |
} ); | |
editor.on( 'keydown', function( event ) { | |
if ( ( canUndo && event.keyCode === 27 /* ESCAPE */ ) || ( canUndo === 'space' && event.keyCode === VK.BACKSPACE ) ) { | |
editor.undoManager.undo(); | |
event.preventDefault(); | |
event.stopImmediatePropagation(); | |
} | |
if ( VK.metaKeyPressed( event ) ) { | |
return; | |
} | |
if ( event.keyCode === VK.ENTER ) { | |
enter(); | |
// Wait for the browser to insert the character. | |
} else if ( event.keyCode === VK.SPACEBAR ) { | |
setTimeout( space ); | |
} else if ( event.keyCode > 47 && ! ( event.keyCode >= 91 && event.keyCode <= 93 ) ) { | |
setTimeout( inline ); | |
} | |
}, true ); | |
function inline() { | |
var rng = editor.selection.getRng(); | |
var node = rng.startContainer; | |
var offset = rng.startOffset; | |
var startOffset; | |
var endOffset; | |
var pattern; | |
var format; | |
var zero; | |
// We need a non-empty text node with an offset greater than zero. | |
if ( ! node || node.nodeType !== 3 || ! node.data.length || ! offset ) { | |
return; | |
} | |
var string = node.data.slice( 0, offset ); | |
var lastChar = node.data.charAt( offset - 1 ); | |
tinymce.each( inlinePatterns, function( p ) { | |
// Character before selection should be delimiter. | |
if ( lastChar !== p.delimiter.slice( -1 ) ) { | |
return; | |
} | |
var escDelimiter = escapeRegExp( p.delimiter ); | |
var delimiterFirstChar = p.delimiter.charAt( 0 ); | |
var regExp = new RegExp( '(.*)' + escDelimiter + '.+' + escDelimiter + '$' ); | |
var match = string.match( regExp ); | |
if ( ! match ) { | |
return; | |
} | |
startOffset = match[1].length; | |
endOffset = offset - p.delimiter.length; | |
var before = string.charAt( startOffset - 1 ); | |
var after = string.charAt( startOffset + p.delimiter.length ); | |
// test*test* => format applied. | |
// test *test* => applied. | |
// test* test* => not applied. | |
if ( startOffset && /\S/.test( before ) ) { | |
if ( /\s/.test( after ) || before === delimiterFirstChar ) { | |
return; | |
} | |
} | |
// Do not replace when only whitespace and delimiter characters. | |
if ( ( new RegExp( '^[\\s' + escapeRegExp( delimiterFirstChar ) + ']+$' ) ).test( string.slice( startOffset, endOffset ) ) ) { | |
return; | |
} | |
pattern = p; | |
return false; | |
} ); | |
if ( ! pattern ) { | |
return; | |
} | |
format = editor.formatter.get( pattern.format ); | |
if ( format && format[0].inline ) { | |
editor.undoManager.add(); | |
editor.undoManager.transact( function() { | |
node.insertData( offset, '\uFEFF' ); | |
node = node.splitText( startOffset ); | |
zero = node.splitText( offset - startOffset ); | |
node.deleteData( 0, pattern.delimiter.length ); | |
node.deleteData( node.data.length - pattern.delimiter.length, pattern.delimiter.length ); | |
editor.formatter.apply( pattern.format, {}, node ); | |
editor.selection.setCursorLocation( zero, 1 ); | |
} ); | |
// We need to wait for native events to be triggered. | |
setTimeout( function() { | |
canUndo = 'space'; | |
editor.once( 'selectionchange', function() { | |
var offset; | |
if ( zero ) { | |
offset = zero.data.indexOf( '\uFEFF' ); | |
if ( offset !== -1 ) { | |
zero.deleteData( offset, offset + 1 ); | |
} | |
} | |
} ); | |
} ); | |
} | |
} | |
function firstTextNode( node ) { | |
var parent = editor.dom.getParent( node, 'p' ), | |
child; | |
if ( ! parent ) { | |
return; | |
} | |
while ( child = parent.firstChild ) { | |
if ( child.nodeType !== 3 ) { | |
parent = child; | |
} else { | |
break; | |
} | |
} | |
if ( ! child ) { | |
return; | |
} | |
if ( ! child.data ) { | |
if ( child.nextSibling && child.nextSibling.nodeType === 3 ) { | |
child = child.nextSibling; | |
} else { | |
child = null; | |
} | |
} | |
return child; | |
} | |
function space() { | |
var rng = editor.selection.getRng(), | |
node = rng.startContainer, | |
parent, | |
text; | |
if ( ! node || firstTextNode( node ) !== node ) { | |
return; | |
} | |
parent = node.parentNode; | |
text = node.data; | |
tinymce.each( spacePatterns, function( pattern ) { | |
var match = text.match( pattern.regExp ); | |
if ( ! match || rng.startOffset !== match[0].length ) { | |
return; | |
} | |
editor.undoManager.add(); | |
editor.undoManager.transact( function() { | |
node.deleteData( 0, match[0].length ); | |
if ( ! parent.innerHTML ) { | |
parent.appendChild( document.createElement( 'br' ) ); | |
} | |
editor.selection.setCursorLocation( parent ); | |
editor.execCommand( pattern.cmd ); | |
} ); | |
// We need to wait for native events to be triggered. | |
setTimeout( function() { | |
canUndo = 'space'; | |
} ); | |
return false; | |
} ); | |
} | |
function enter() { | |
var rng = editor.selection.getRng(), | |
start = rng.startContainer, | |
node = firstTextNode( start ), | |
i = enterPatterns.length, | |
text, pattern, parent; | |
if ( ! node ) { | |
return; | |
} | |
text = node.data; | |
while ( i-- ) { | |
if ( enterPatterns[ i ].start ) { | |
if ( text.indexOf( enterPatterns[ i ].start ) === 0 ) { | |
pattern = enterPatterns[ i ]; | |
break; | |
} | |
} else if ( enterPatterns[ i ].regExp ) { | |
if ( enterPatterns[ i ].regExp.test( text ) ) { | |
pattern = enterPatterns[ i ]; | |
break; | |
} | |
} | |
} | |
if ( ! pattern ) { | |
return; | |
} | |
if ( node === start && tinymce.trim( text ) === pattern.start ) { | |
return; | |
} | |
editor.once( 'keyup', function() { | |
editor.undoManager.add(); | |
editor.undoManager.transact( function() { | |
if ( pattern.format ) { | |
editor.formatter.apply( pattern.format, {}, node ); | |
node.replaceData( 0, node.data.length, ltrim( node.data.slice( pattern.start.length ) ) ); | |
} else if ( pattern.element ) { | |
parent = node.parentNode && node.parentNode.parentNode; | |
if ( parent ) { | |
parent.replaceChild( document.createElement( pattern.element ), node.parentNode ); | |
} | |
} | |
} ); | |
// We need to wait for native events to be triggered. | |
setTimeout( function() { | |
canUndo = 'enter'; | |
} ); | |
} ); | |
} | |
function ltrim( text ) { | |
return text ? text.replace( /^\s+/, '' ) : ''; | |
} | |
} ); | |
} )( window.tinymce, window.setTimeout ); | |