Adding a new Quick Tag in WordPress

There is a very simple way to modify quicktags-toolbar in WordPress.

quick tags toolbar

The “Quick Tags” are JQuery driven and here is the code responsible for them (from /wp-includes/quicktags.dev.js) :

# ensure backward compatibility
edButtons[10] = new qt.TagButton('strong','b','<strong>','</strong>','b');
edButtons[20] = new qt.TagButton('em','i','<em>','</em>','i'),
edButtons[30] = new qt.LinkButton(), // special case
edButtons[40] = new qt.TagButton('block','b-quote','<blockquote>','</blockquote>','q'),
edButtons[50] = new qt.TagButton('del','del','<del datetime='' + _datetime + ''>','</del>','d'),
edButtons[60] = new qt.TagButton('ins','ins','<ins datetime='' + _datetime + ''>','</ins>','s'),
edButtons[70] = new qt.ImgButton(), // special case
edButtons[80] = new qt.TagButton('ul','ul','<ul>','</ul>','u'),
edButtons[90] = new qt.TagButton('ol','ol','<ol>','</ol>','o'),
edButtons[100] = new qt.TagButton('li','li','<li>','</li>','l'),
edButtons[110] = new qt.TagButton('code','code','`','`','c'),
edButtons[120] = new qt.TagButton('more','more','<!--more-->','','t'),
edButtons[130] = new qt.SpellButton(),
edButtons[140] = new qt.CloseButton()

We can add the <pre> button similar to the <code> button, but we do not like to edit this file directly, because it is bad practice to edit the WordPress core files.

Our solution is to reassign quicktags to load from our custom file:

if (is_admin()) {
  add_action('init', pr_load_admin_scripts);
}
function pr_load_admin_scripts()
{
if ( is_admin() ) {
wp_deregister_script('quicktags');
wp_register_script('quicktags', get_bloginfo('template_url').'/js/quicktags.js', false, '', true);
}
}
add_filter( 'tiny_mce_version', 'my_refresh_mce');
function my_refresh_mce($ver) {
  $ver += 3;
  return $ver;
}

My quicktags.js looks like this and is stored in the theme /js folder.

var QTags, edButtons = [], edCanvas,
/**
 * Back-compat
 *
 * Define all former global functions so plugins that hack quicktags.js directly don't cause fatal errors.
 */
edAddTag = function(){},
edCheckOpenTags = function(){},
edCloseAllTags = function(){},
edInsertImage = function(){},
edInsertLink = function(){},
edInsertTag = function(){},
edLink = function(){},
edQuickLink = function(){},
edRemoveTag = function(){},
edShowButton = function(){},
edShowLinks = function(){},
edSpell = function(){},
edToolbar = function(){};
/**
 * Initialize new instance of the Quicktags editor
 */
function quicktags(settings) {
    return new QTags(settings);
}
/**
 * Inserts content at the caret in the active editor (textarea)
 *
 * Added for back compatibility
 * @see QTags.insertContent()
 */
function edInsertContent(bah, txt) {
    return QTags.insertContent(txt);
}
/**
 * Adds a button to all instances of the editor
 *
 * Added for back compatibility, use QTags.addButton() as it gives more flexibility like type of button, button placement, etc.
 * @see QTags.addButton()
 */
function edButton(id, display, tagStart, tagEnd, access, open) {
    return QTags.addButton( id, display, tagStart, tagEnd, access, '', -1 );
}
(function(){
    // private stuff is prefixed with an underscore
    var _domReady = function(func) {
        var t, i,  DOMContentLoaded;
        if ( typeof jQuery != 'undefined' ) {
            jQuery(document).ready(func);
        } else {
            t = _domReady;
            t.funcs = [];
            t.ready = function() {
                if ( ! t.isReady ) {
                    t.isReady = true;
                    for ( i = 0; i < t.funcs.length; i++ ) {
                        t.funcs[i]();
                    }
                }
            };
            if ( t.isReady ) {
                func();
            } else {
                t.funcs.push(func);
            }
            if ( ! t.eventAttached ) {
                if ( document.addEventListener ) {
                    DOMContentLoaded = function(){document.removeEventListener('DOMContentLoaded', DOMContentLoaded, false);t.ready();};
                    document.addEventListener('DOMContentLoaded', DOMContentLoaded, false);
                    window.addEventListener('load', t.ready, false);
                } else if ( document.attachEvent ) {
                    DOMContentLoaded = function(){if (document.readyState === 'complete'){ document.detachEvent('onreadystatechange', DOMContentLoaded);t.ready();}};
                    document.attachEvent('onreadystatechange', DOMContentLoaded);
                    window.attachEvent('onload', t.ready);
                    (function(){
                        try {
                            document.documentElement.doScroll('left');
                        } catch(e) {
                            setTimeout(arguments.callee, 50);
                            return;
                        }
                        t.ready();
                    })();
                }
                t.eventAttached = true;
            }
        }
    },
    _datetime = (function() {
        var now = new Date(), zeroise;
        zeroise = function(number) {
            var str = number.toString();
            if ( str.length < 2 )
                str = '0' + str;
            return str;
        }
        return now.getUTCFullYear() + '-' +
            zeroise( now.getUTCMonth() + 1 ) + '-' +
            zeroise( now.getUTCDate() ) + 'T' +
            zeroise( now.getUTCHours() ) + ':' +
            zeroise( now.getUTCMinutes() ) + ':' +
            zeroise( now.getUTCSeconds() ) +
            '+00:00';
    })(),
    qt;
    qt = QTags = function(settings) {
        if ( typeof(settings) == 'string' )
            settings = {id: settings};
        else if ( typeof(settings) != 'object' )
            return false;
        var t = this,
            id = settings.id,
            canvas = document.getElementById(id),
            name = 'qt_' + id,
            tb, onclick, toolbar_id;
        if ( !id || !canvas )
            return false;
        t.name = name;
        t.id = id;
        t.canvas = canvas;
        t.settings = settings;
        if ( id == 'content' &amp;&amp; typeof(adminpage) == 'string' &amp;&amp; ( adminpage == 'post-new-php' || adminpage == 'post-php' ) ) {
            // back compat hack :-(
            edCanvas = canvas;
            toolbar_id = 'ed_toolbar';
        } else {
            toolbar_id = name + '_toolbar';
        }
        tb = document.createElement('div');
        tb.id = toolbar_id;
        tb.className = 'quicktags-toolbar';
        canvas.parentNode.insertBefore(tb, canvas);
        t.toolbar = tb;
        // listen for click events
        onclick = function(e) {
            e = e || window.event;
            var target = e.target || e.srcElement, i;
            // as long as it has the class ed_button, execute the callback
            if ( / ed_button /.test(' ' + target.className + ' ') ) {
                // we have to reassign canvas here
                t.canvas = canvas = document.getElementById(id);
                i = target.id.replace(name + '_', '');
                if ( t.theButtons[i] )
                    t.theButtons[i].callback.call(t.theButtons[i], target, canvas, t);
            }
        };
        if ( tb.addEventListener ) {
            tb.addEventListener('click', onclick, false);
        } else if ( tb.attachEvent ) {
            tb.attachEvent('onclick', onclick);
        }
        t.getButton = function(id) {
            return t.theButtons[id];
        };
        t.getButtonElement = function(id) {
            return document.getElementById(name + '_' + id);
        };
        qt.instances[id] = t;
        if ( !qt.instances[0] ) {
            qt.instances[0] = qt.instances[id];
            _domReady( function(){ qt._buttonsInit(); } );
        }
    };
    qt.instances = {};
    qt.getInstance = function(id) {
        return qt.instances[id];
    };
    qt._buttonsInit = function() {
        var t = this, canvas, name, settings, theButtons, html, inst, ed, id, i, use,
            defaults = ',strong,em,link,block,del,ins,img,ul,ol,li,code,more,spell,close,';
        for ( inst in t.instances ) {
            if ( inst == 0 )
                continue;
            ed = t.instances[inst];
            canvas = ed.canvas;
            name = ed.name;
            settings = ed.settings;
            html = '';
            theButtons = {};
            use = '';
            // set buttons
            if ( settings.buttons )
                use = ','+settings.buttons+',';
            for ( i in edButtons ) {
                if ( !edButtons[i] )
                    continue;
                id = edButtons[i].id;
                if ( use &amp;&amp; defaults.indexOf(','+id+',') != -1 &amp;&amp; use.indexOf(','+id+',') == -1 )
                    continue;
                if ( !edButtons[i].instance || edButtons[i].instance == inst ) {
                    theButtons[id] = edButtons[i];
                    if ( edButtons[i].html )
                        html += edButtons[i].html(name + '_');
                }
            }
            if ( use &amp;&amp; use.indexOf(',fullscreen,') != -1 ) {
                theButtons['fullscreen'] = new qt.FullscreenButton();
                html += theButtons['fullscreen'].html(name + '_');
            }
 
            if ( 'rtl' == document.getElementsByTagName('html')[0].dir ) {
                theButtons['textdirection'] = new qt.TextDirectionButton();
                html += theButtons['textdirection'].html(name + '_');
            }
            ed.toolbar.innerHTML = html;
            ed.theButtons = theButtons;
        }
        t.buttonsInitDone = true;
    };
    /**
     * Main API function for adding a button to Quicktags
     *
     * Adds qt.Button or qt.TagButton depending on the args. The first three args are always required.
     * To be able to add button(s) to Quicktags, your script should be enqueued as dependent
     * on 'quicktags' and outputted in the footer. If you are echoing JS directly from PHP,
     * use add_action( 'admin_print_footer_scripts', 'output_my_js', 100 ) or add_action( 'wp_footer', 'output_my_js', 100 )
     *
     * Minimum required to add a button that calls an external function:
     *     QTags.addButton( 'my_id', 'my button', my_callback );
     *     function my_callback() { alert('yeah!'); }
     *
     * Minimum required to add a button that inserts a tag:
     *     QTags.addButton( 'my_id', 'my button', '<span>', '</span>' );
     *     QTags.addButton( 'my_id2', 'my button', '<br />' );
     *
     * @param id string required Button HTML ID
     * @param display string required Button's value='...'
     * @param arg1 string || function required Either a starting tag to be inserted like '<span>' or a callback that is executed when the button is clicked.
     * @param arg2 string optional Ending tag like '</span>'
     * @param access_key string optional Access key for the button.
     * @param title string optional Button's title='...'
     * @param priority int optional Number representing the desired position of the button in the toolbar. 1 - 9 = first, 11 - 19 = second, 21 - 29 = third, etc.
     * @param instance string optional Limit the button to a specific instance of Quicktags, add to all instances if not present.
     * @return mixed null or the button object that is needed for back-compat.
     */
    qt.addButton = function( id, display, arg1, arg2, access_key, title, priority, instance ) {
        var btn;
        if ( !id || !display )
            return;
        priority = priority || 0;
        arg2 = arg2 || '';
        if ( typeof(arg1) === 'function' ) {
            btn = new qt.Button(id, display, access_key, title, instance);
            btn.callback = arg1;
        } else if ( typeof(arg1) === 'string' ) {
            btn = new qt.TagButton(id, display, arg1, arg2, access_key, title, instance);
        } else {
            return;
        }
        if ( priority == -1 ) // back-compat
            return btn;
        if ( priority > 0 ) {
            while ( typeof(edButtons[priority]) != 'undefined' ) {
                priority++
            }
            edButtons[priority] = btn;
        } else {
            edButtons[edButtons.length] = btn;
        }
        if ( this.buttonsInitDone )
            this._buttonsInit(); // add the button HTML to all instances toolbars if addButton() was called too late
    };
    qt.insertContent = function(content) {
        var sel, startPos, endPos, scrollTop, text, canvas = document.getElementById(wpActiveEditor);
        if ( !canvas )
            return false;
        if ( document.selection ) { //IE
            canvas.focus();
            sel = document.selection.createRange();
            sel.text = content;
            canvas.focus();
        } else if ( canvas.selectionStart || canvas.selectionStart == '0' ) { // FF, WebKit, Opera
            text = canvas.value;
            startPos = canvas.selectionStart;
            endPos = canvas.selectionEnd;
            scrollTop = canvas.scrollTop;
            canvas.value = text.substring(0, startPos) + content + text.substring(endPos, text.length);
            canvas.focus();
            canvas.selectionStart = startPos + content.length;
            canvas.selectionEnd = startPos + content.length;
            canvas.scrollTop = scrollTop;
        } else {
            canvas.value += content;
            canvas.focus();
        }
        return true;
    };
    // a plain, dumb button
    qt.Button = function(id, display, access, title, instance) {
        var t = this;
        t.id = id;
        t.display = display;
        t.access = access;
        t.title = title || '';
        t.instance = instance || '';
    };
    qt.Button.prototype.html = function(idPrefix) {
        var access = this.access ? ' accesskey='' + this.access + ''' : '';
        return '<input type='button' id='' + idPrefix + this.id + ''' + access + ' class='ed_button' title='' + this.title + '' value='' + this.display + '' />';
    };
    qt.Button.prototype.callback = function(){};
    // a button that inserts HTML tag
    qt.TagButton = function(id, display, tagStart, tagEnd, access, title, instance) {
        var t = this;
        qt.Button.call(t, id, display, access, title, instance);
        t.tagStart = tagStart;
        t.tagEnd = tagEnd;
    };
    qt.TagButton.prototype = new qt.Button();
    qt.TagButton.prototype.openTag = function(e, ed) {
        var t = this;
        if ( ! ed.openTags ) {
            ed.openTags = [];
        }
        if ( t.tagEnd ) {
            ed.openTags.push(t.id);
            e.value = '/' + e.value;
        }
    };
    qt.TagButton.prototype.closeTag = function(e, ed) {
        var t = this, i = t.isOpen(ed);
        if ( i !== false ) {
            ed.openTags.splice(i, 1);
        }
        e.value = t.display;
    };
    // whether a tag is open or not. Returns false if not open, or current open depth of the tag
    qt.TagButton.prototype.isOpen = function (ed) {
        var t = this, i = 0, ret = false;
        if ( ed.openTags ) {
            while ( ret === false &amp;&amp; i < ed.openTags.length ) {
                ret = ed.openTags[i] == t.id ? i : false;
                i ++;
            }
        } else {
            ret = false;
        }
        return ret;
    };
    qt.TagButton.prototype.callback = function(element, canvas, ed) {
        var t = this, startPos, endPos, cursorPos, scrollTop, v = canvas.value, l, r, i, sel, endTag = v ? t.tagEnd : '';
        if ( document.selection ) { // IE
            canvas.focus();
            sel = document.selection.createRange();
            if ( sel.text.length > 0 ) {
                if ( !t.tagEnd )
                    sel.text = sel.text + t.tagStart;
                else
                    sel.text = t.tagStart + sel.text + endTag;
            } else {
                if ( !t.tagEnd ) {
                    sel.text = t.tagStart;
                } else if ( t.isOpen(ed) === false ) {
                    sel.text = t.tagStart;
                    t.openTag(element, ed);
                } else {
                    sel.text = endTag;
                    t.closeTag(element, ed);
                }
            }
            canvas.focus();
        } else if ( canvas.selectionStart || canvas.selectionStart == '0' ) { // FF, WebKit, Opera
            startPos = canvas.selectionStart;
            endPos = canvas.selectionEnd;
            cursorPos = endPos;
            scrollTop = canvas.scrollTop;
            l = v.substring(0, startPos); // left of the selection
            r = v.substring(endPos, v.length); // right of the selection
            i = v.substring(startPos, endPos); // inside the selection
            if ( startPos != endPos ) {
                if ( !t.tagEnd ) {
                    canvas.value = l + i + t.tagStart + r; // insert self closing tags after the selection
                    cursorPos += t.tagStart.length;
                } else {
                    canvas.value = l + t.tagStart + i + endTag + r;
                    cursorPos += t.tagStart.length + endTag.length;
                }
            } else {
                if ( !t.tagEnd ) {
                    canvas.value = l + t.tagStart + r;
                    cursorPos = startPos + t.tagStart.length;
                } else if ( t.isOpen(ed) === false ) {
                    canvas.value = l + t.tagStart + r;
                    t.openTag(element, ed);
                    cursorPos = startPos + t.tagStart.length;
                } else {
                    canvas.value = l + endTag + r;
                    cursorPos = startPos + endTag.length;
                    t.closeTag(element, ed);
                }
            }
            canvas.focus();
            canvas.selectionStart = cursorPos;
            canvas.selectionEnd = cursorPos;
            canvas.scrollTop = scrollTop;
        } else { // other browsers?
            if ( !endTag ) {
                canvas.value += t.tagStart;
            } else if ( t.isOpen(ed) !== false ) {
                canvas.value += t.tagStart;
                t.openTag(element, ed);
            } else {
                canvas.value += endTag;
                t.closeTag(element, ed);
            }
            canvas.focus();
        }
    };
    // the spell button
    qt.SpellButton = function() {
        qt.Button.call(this, 'spell', 'Spell', '', 'dictionaryLookup');
    };
    qt.SpellButton.prototype = new qt.Button();
    qt.SpellButton.prototype.callback = function(element, canvas, ed) {
        var word = '', sel, startPos, endPos;
        if ( document.selection ) {
            canvas.focus();
            sel = document.selection.createRange();
            if ( sel.text.length > 0 ) {
                word = sel.text;
            }
        } else if ( canvas.selectionStart || canvas.selectionStart == '0' ) {
            startPos = canvas.selectionStart;
            endPos = canvas.selectionEnd;
            if ( startPos != endPos ) {
                word = canvas.value.substring(startPos, endPos);
            }
        }
        if ( word === '' ) {
            word = prompt('Word Lookup', '');
        }
        if ( word !== null &amp;&amp; /^\w[\w ]*$/.test(word)) {
            window.open('http://www.answers.com/' + encodeURIComponent(word));
        }
    };
    // the close tags button
    qt.CloseButton = function() {
        qt.Button.call(this, 'close', 'closeTags', '', 'closeAllOpenTags');
    };
    qt.CloseButton.prototype = new qt.Button();
    qt._close = function(e, c, ed) {
        var button, element, tbo = ed.openTags;
        if ( tbo ) {
            while ( tbo.length > 0 ) {
                button = ed.getButton(tbo[tbo.length - 1]);
                element = document.getElementById(ed.name + '_' + button.id);
                if ( e )
                    button.callback.call(button, element, c, ed);
                else
                    button.closeTag(element, ed);
            }
        }
    };
    qt.CloseButton.prototype.callback = qt._close;
    qt.closeAllTags = function(editor_id) {
        var ed = this.getInstance(editor_id);
        qt._close('', ed.canvas, ed);
    };
    // the link button
    qt.LinkButton = function() {
        qt.TagButton.call(this, 'link', 'link', '', '</a>', 'a');
    };
    qt.LinkButton.prototype = new qt.TagButton();
    qt.LinkButton.prototype.callback = function(e, c, ed, defaultValue) {
        var URL, t = this;
        if ( typeof(wpLink) != 'undefined' ) {
            wpLink.open();
            return;
        }
        if ( ! defaultValue )
            defaultValue = 'http://';
        if ( t.isOpen(ed) === false ) {
            URL = prompt('enterURL', defaultValue);
            if ( URL ) {
                t.tagStart = '<a href='' + URL + ''>';
                qt.TagButton.prototype.callback.call(t, e, c, ed);
            }
        } else {
            qt.TagButton.prototype.callback.call(t, e, c, ed);
        }
    };
    // the img button
    qt.ImgButton = function() {
        qt.TagButton.call(this, 'img', 'img', '', '', 'm');
    };
    qt.ImgButton.prototype = new qt.TagButton();
    qt.ImgButton.prototype.callback = function(e, c, ed, defaultValue) {
        if ( ! defaultValue ) {
            defaultValue = 'http://';
        }
        var src = prompt('enterImageURL', defaultValue), alt;
        if ( src ) {
            alt = prompt('enterImageDescription', '');
            this.tagStart = '<img src='' + src + '' alt='' + alt + '' />';
            qt.TagButton.prototype.callback.call(this, e, c, ed);
        }
    };
    qt.FullscreenButton = function() {
        qt.Button.call(this, 'fullscreen', 'fullscreen', 'f', 'toggleFullscreen');
    };
    qt.FullscreenButton.prototype = new qt.Button();
    qt.FullscreenButton.prototype.callback = function(e, c) {
        if ( !c.id || typeof(fullscreen) == 'undefined' )
            return;
        fullscreen.on();
    };
    qt.TextDirectionButton = function() {
        qt.Button.call(this, 'textdirection', 'textdirection', '', 'toggleTextdirection')
    };
    qt.TextDirectionButton.prototype = new qt.Button();
    qt.TextDirectionButton.prototype.callback = function(e, c) {
        var isRTL = ( 'rtl' == document.getElementsByTagName('html')[0].dir ),
            currentDirection = c.style.direction;
        if ( ! currentDirection )
            currentDirection = ( isRTL ) ? 'rtl' : 'ltr';
        c.style.direction = ( 'rtl' == currentDirection ) ? 'ltr' : 'rtl';
        c.focus();
    }
    // ensure backward compatibility
    edButtons[10] = new qt.TagButton('strong','b','<strong>','</strong>','b');
    edButtons[20] = new qt.TagButton('em','i','<em>','</em>','i'),
    edButtons[30] = new qt.LinkButton(), // special case
    edButtons[40] = new qt.TagButton('block','b-quote','\n\n<blockquote>','</blockquote>\n\n','q'),
    edButtons[50] = new qt.TagButton('del','del','<del datetime='' + _datetime + ''>','</del>','d'),
    edButtons[60] = new qt.TagButton('ins','ins','<ins datetime='' + _datetime + ''>','</ins>','s'),
    edButtons[70] = new qt.ImgButton(), // special case
    edButtons[80] = new qt.TagButton('ul','ul','<ul>\n','</ul>\n\n','u'),
    edButtons[90] = new qt.TagButton('ol','ol','<ol>\n','</ol>\n\n','o'),
    edButtons[100] = new qt.TagButton('li','li','\t<li>','</li>\n','l'),
    edButtons[110] = new qt.TagButton('code','code','`','`','c'),
    edButtons[120] = new qt.TagButton('more','more','<!--more-->','','t'),
    edButtons[125] = new qt.TagButton('pre','pre','<pre>','</pre>','p'),
    edButtons[130] = new qt.SpellButton(),
    edButtons[140] = new qt.CloseButton()
})();

Finally, after we add the <pre> tag the final result is like this:

pre

tags: quick - tags & category: wordpress