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
This commit is contained in:
parent
66f327cff2
commit
5bac5f7ccd
@ -368,7 +368,8 @@ final class _WP_Editors {
|
||||
'wpgallery',
|
||||
'wplink',
|
||||
'wpdialogs',
|
||||
'wpview',
|
||||
'wptextpattern',
|
||||
'wpview'
|
||||
);
|
||||
|
||||
if ( ! self::$has_medialib ) {
|
||||
|
100
src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
Normal file
100
src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
Normal file
@ -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( '<br>' );
|
||||
}
|
||||
|
||||
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 );
|
@ -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 <p>abc|</p> and after the abc text node then move it to the end of the text node
|
||||
// Expand the range to include the last char <p>ab[c]</p> 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 <p>abc|</p> and after the abc text node then move it to the end of the text node
|
||||
// Expand the range to include the last char <p>ab[c]</p> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,5 +108,11 @@
|
||||
<# } #>
|
||||
</li>
|
||||
</script>
|
||||
|
||||
<!-- TinyMCE -->
|
||||
|
||||
<script src="../../src/wp-includes/js/tinymce/tinymce.js"></script>
|
||||
<script src="editor/js/utils.js"></script>
|
||||
<script src="wp-includes/js/tinymce/plugins/wptextpattern/plugin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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( '<textarea id="editor">' );
|
||||
|
||||
tinymce.init( {
|
||||
selector: '#editor',
|
||||
plugins: 'wptextpattern',
|
||||
init_instance_callback: function() {
|
||||
editor = arguments[0];
|
||||
editor.focus();
|
||||
editor.selection.setCursorLocation();
|
||||
setTimeout( done );
|
||||
}
|
||||
} );
|
||||
},
|
||||
afterEach: function() {
|
||||
editor.remove();
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'Unordered list.', function( assert ) {
|
||||
type( '* test', function() {
|
||||
assert.equal( editor.getContent(), '<ul>\n<li>test</li>\n</ul>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Ordered list.', function( assert ) {
|
||||
type( '1. test', function() {
|
||||
assert.equal( editor.getContent(), '<ol>\n<li>test</li>\n</ol>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Ordered list with content.', function( assert ) {
|
||||
editor.setContent( '<p><strong>test</strong></p>' );
|
||||
editor.selection.setCursorLocation();
|
||||
|
||||
type( '* ', function() {
|
||||
assert.equal( editor.getContent(), '<ul>\n<li><strong>test</strong></li>\n</ul>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Only transform inside a P tag.', function( assert ) {
|
||||
editor.setContent( '<h1>test</h1>' );
|
||||
editor.selection.setCursorLocation();
|
||||
|
||||
type( '* ', function() {
|
||||
assert.equal( editor.getContent(), '<h1>* test</h1>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Only transform at the start of a P tag.', function( assert ) {
|
||||
editor.setContent( '<p>test <strong>test</strong></p>' );
|
||||
editor.selection.setCursorLocation( editor.$( 'strong' )[0].firstChild, 0 );
|
||||
|
||||
type( '* ', function() {
|
||||
assert.equal( editor.getContent(), '<p>test <strong>* test</strong></p>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Only transform when at the cursor is at the start.', function( assert ) {
|
||||
editor.setContent( '<p>* test</p>' );
|
||||
editor.selection.setCursorLocation( editor.$( 'p' )[0].firstChild, 6 );
|
||||
|
||||
type( ' test', function() {
|
||||
assert.equal( editor.getContent(), '<p>* test test</p>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Backspace should undo the transformation.', function( assert ) {
|
||||
editor.setContent( '<p>test</p>' );
|
||||
editor.selection.setCursorLocation();
|
||||
|
||||
type( '* \b', function() {
|
||||
assert.equal( editor.getContent(), '<p>* test</p>' );
|
||||
assert.equal( editor.selection.getRng().startOffset, 2 );
|
||||
}, assert.async() );
|
||||
} );
|
||||
|
||||
QUnit.test( 'Backspace should undo the transformation only right after it happened.', function( assert ) {
|
||||
editor.setContent( '<p>test</p>' );
|
||||
editor.selection.setCursorLocation();
|
||||
|
||||
type( '* ', function() {
|
||||
editor.selection.setCursorLocation( editor.$( 'li' )[0].firstChild, 4 );
|
||||
// Gecko.
|
||||
editor.fire( 'click' );
|
||||
}, '\b', function() {
|
||||
assert.equal( editor.getContent(), '<ul>\n<li>tes</li>\n</ul>' );
|
||||
}, assert.async() );
|
||||
} );
|
||||
} )( window.jQuery, window.QUnit, window.tinymce, window.Utils.type, window.setTimeout );
|
Loading…
Reference in New Issue
Block a user