diff --git a/src/wp-includes/class-wp-editor.php b/src/wp-includes/class-wp-editor.php index 7bccc2f0f1..acd789532c 100644 --- a/src/wp-includes/class-wp-editor.php +++ b/src/wp-includes/class-wp-editor.php @@ -368,7 +368,8 @@ final class _WP_Editors { 'wpgallery', 'wplink', 'wpdialogs', - 'wpview', + 'wptextpattern', + 'wpview' ); if ( ! self::$has_medialib ) { diff --git a/src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js b/src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js new file mode 100644 index 0000000000..1b7df4fb4a --- /dev/null +++ b/src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js @@ -0,0 +1,100 @@ +( function( tinymce, setTimeout ) { + tinymce.PluginManager.add( 'wptextpattern', function( editor ) { + var $$ = editor.$, + patterns = [], + canUndo = false; + + function add( regExp, callback ) { + patterns.push( { + regExp: regExp, + callback: callback + } ); + } + + add( /^[*-]\s/, function() { + this.execCommand( 'InsertUnorderedList' ); + } ); + + add( /^1[.)]\s/, function() { + this.execCommand( 'InsertOrderedList' ); + } ); + + editor.on( 'selectionchange', function() { + canUndo = false; + } ); + + editor.on( 'keydown', function( event ) { + if ( canUndo && event.keyCode === tinymce.util.VK.BACKSPACE ) { + editor.undoManager.undo(); + event.preventDefault(); + } + } ); + + editor.on( 'keyup', function( event ) { + var rng, node, text, parent, child; + + if ( event.keyCode !== tinymce.util.VK.SPACEBAR ) { + return; + } + + rng = editor.selection.getRng(); + node = rng.startContainer; + text = node.nodeValue; + + if ( node.nodeType !== 3 ) { + return; + } + + parent = editor.dom.getParent( node, 'p' ); + + if ( ! parent ) { + return; + } + + while ( child = parent.firstChild ) { + if ( child.nodeType !== 3 ) { + parent = child; + } else { + break; + } + } + + if ( child !== node ) { + return; + } + + tinymce.each( patterns, function( pattern ) { + var replace = text.replace( pattern.regExp, '' ); + + if ( text === replace ) { + return; + } + + if ( rng.startOffset !== text.length - replace.length ) { + return; + } + + editor.undoManager.add(); + + editor.undoManager.transact( function() { + if ( replace ) { + $$( node ).replaceWith( document.createTextNode( replace ) ); + } else { + $$( node.parentNode ).empty().append( '
' ); + } + + editor.selection.setCursorLocation( parent ); + + pattern.callback.apply( editor ); + } ); + + // We need to wait for native events to be triggered. + setTimeout( function() { + canUndo = true; + } ); + + return false; + } ); + } ); + } ); +} )( window.tinymce, window.setTimeout ); diff --git a/tests/qunit/editor/js/utils.js b/tests/qunit/editor/js/utils.js index 17e2644436..570dafe9ac 100644 --- a/tests/qunit/editor/js/utils.js +++ b/tests/qunit/editor/js/utils.js @@ -131,7 +131,18 @@ // TODO: Replace this with the new event logic in 3.5 function type(chr) { - var editor = tinymce.activeEditor, keyCode, charCode, evt, startElm, rng; + var editor = tinymce.activeEditor, keyCode, charCode, evt, startElm, rng, startContainer, startOffset, textNode; + + function charCodeToKeyCode(charCode) { + var lookup = { + '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57,'a': 65, 'b': 66, 'c': 67, + 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, + 'r': 82, 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, ' ': 32, ',': 188, '-': 189, '.': 190, '/': 191, '\\': 220, + '[': 219, ']': 221, '\'': 222, ';': 186, '=': 187, ')': 41 + }; + + return lookup[String.fromCharCode(charCode)]; + } function fakeEvent(target, type, evt) { editor.dom.fire(target, type, evt); @@ -139,7 +150,8 @@ // Numeric keyCode if (typeof(chr) == "number") { - charCode = keyCode = chr; + charCode = chr; + keyCode = charCodeToKeyCode(charCode); } else if (typeof(chr) == "string") { // String value if (chr == '\b') { @@ -150,10 +162,18 @@ charCode = chr.charCodeAt(0); } else { charCode = chr.charCodeAt(0); - keyCode = charCode; + keyCode = charCodeToKeyCode(charCode); } } else { evt = chr; + + if (evt.charCode) { + chr = String.fromCharCode(evt.charCode); + } + + if (evt.keyCode) { + keyCode = evt.keyCode; + } } evt = evt || {keyCode: keyCode, charCode: charCode}; @@ -175,17 +195,19 @@ rng.execCommand('Delete', false, null); } else { rng = editor.selection.getRng(); + startContainer = rng.startContainer; - if (rng.startContainer.nodeType == 1 && rng.collapsed) { - var nodes = rng.startContainer.childNodes, lastNode = nodes[nodes.length - 1]; + if (startContainer.nodeType == 1 && rng.collapsed) { + var nodes = rng.startContainer.childNodes; + startContainer = nodes[nodes.length - 1]; + } - // If caret is at

abc|

and after the abc text node then move it to the end of the text node - // Expand the range to include the last char

ab[c]

since IE 11 doesn't delete otherwise - if (rng.startOffset >= nodes.length - 1 && lastNode && lastNode.nodeType == 3 && lastNode.data.length > 0) { - rng.setStart(lastNode, lastNode.data.length - 1); - rng.setEnd(lastNode, lastNode.data.length); - editor.selection.setRng(rng); - } + // If caret is at

abc|

and after the abc text node then move it to the end of the text node + // Expand the range to include the last char

ab[c]

since IE 11 doesn't delete otherwise + if ( rng.collapsed && startContainer && startContainer.nodeType == 3 && startContainer.data.length > 0) { + rng.setStart(startContainer, startContainer.data.length - 1); + rng.setEnd(startContainer, startContainer.data.length); + editor.selection.setRng(rng); } editor.getDoc().execCommand('Delete', false, null); @@ -194,13 +216,19 @@ rng = editor.selection.getRng(true); if (rng.startContainer.nodeType == 3 && rng.collapsed) { - rng.startContainer.insertData(rng.startOffset, chr); - rng.setStart(rng.startContainer, rng.startOffset + 1); - rng.collapse(true); - editor.selection.setRng(rng); + // `insertData` may alter the range. + startContainer = rng.startContainer; + startOffset = rng.startOffset; + rng.startContainer.insertData( rng.startOffset, chr ); + rng.setStart( startContainer, startOffset + 1 ); } else { - rng.insertNode(editor.getDoc().createTextNode(chr)); + textNode = editor.getDoc().createTextNode(chr); + rng.insertNode(textNode); + rng.setStart(textNode, 1); } + + rng.collapse(true); + editor.selection.setRng(rng); } } diff --git a/tests/qunit/index.html b/tests/qunit/index.html index 2adf20bbb2..6f11295bb3 100644 --- a/tests/qunit/index.html +++ b/tests/qunit/index.html @@ -108,5 +108,11 @@ <# } #> + + + + + + diff --git a/tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js b/tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js new file mode 100644 index 0000000000..3c929c52ec --- /dev/null +++ b/tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js @@ -0,0 +1,120 @@ +( function( $, QUnit, tinymce, _type, setTimeout ) { + var editor; + + function type() { + var args = arguments; + + setTimeout( function() { + if ( typeof args[0] === 'string' ) { + args[0] = args[0].split( '' ); + } + + if ( typeof args[0] === 'function' ) { + args[0](); + } else { + _type( args[0].shift() ); + } + + if ( ! args[0].length ) { + [].shift.call( args ); + } + + if ( args.length ) { + type.apply( null, args ); + } + } ); + } + + QUnit.module( 'tinymce.plugins.wptextpattern', { + beforeEach: function( assert ) { + var done = assert.async(); + + $( '#qunit-fixture' ).append( '