From 5bac5f7ccdbf189f287f31a88fc79ca5f23baee5 Mon Sep 17 00:00:00 2001 From: Ella Iseulde Van Dorpe Date: Sat, 6 Jun 2015 20:07:00 +0000 Subject: [PATCH] TinyMCE: add wptextpattern plugin This plugin can automatically format text patterns as you type. It includes two patterns: unordered (`* ` and `- `) and ordered list (`1. ` and `1) `). 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. This is the first TinyMCE plugin that has unit tests and there's some good groundwork for adding tests to existing plugins in the future. First run. See #31441. git-svn-id: https://develop.svn.wordpress.org/trunk@32699 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-editor.php | 3 +- .../tinymce/plugins/wptextpattern/plugin.js | 100 +++++++++++++++ tests/qunit/editor/js/utils.js | 62 ++++++--- tests/qunit/index.html | 6 + .../tinymce/plugins/wptextpattern/plugin.js | 120 ++++++++++++++++++ 5 files changed, 273 insertions(+), 18 deletions(-) create mode 100644 src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js create mode 100644 tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js 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( '