Dodawanie własnych przycisków w edytorze TinyMCE 4.*

WordPress 3.9 zawiera aktualizację edytora wpisów TinyMCE do wersji 4.*. Oznacza to spore zmiany w API edytora – dlatego postanowiłem opisać dodawanie własnych przycisków do edytora z użyciem nowego API.

Spis treści

Kod z poniższych przykładów można znaleźć też na Githubie.

  1. Deklarowanie nowego przycisku TinyMCE
  2. Przycisk z etykietą tekstową
  3. Przycisk ze standardową ikonką
  4. Przycisk z niestandardową ikonką pochodzącą z Dashicons
  5. Własna grafika jako ikonka przycisku
  6. Dodajemy przyciskowi submenu
  7. Dodajemy też sub-submenu 😉
  8. Dodajemy popup po kliknięciu
  9. Rozbudowywujemy popup

Deklarowanie nowego przycisku TinyMCE

Zacznijmy od podstaw – aby w ogóle nasze przyciski pojawiły się w edytorze należy podpiąć się pod akcję admin_head:

add_action('admin_head', 'dziudek_add_my_tc_button');

Funkcja dziudek_add_my_tc_button powinna mieć następującą postać:

function dziudek_add_my_tc_button() {
    global $typenow;
    // sprawdzamy czy user ma uprawnienia do edycji postów/podstron
    if ( !current_user_can('edit_posts') && !current_user_can('edit_pages') ) {
   	return;
    }
    // weryfikujemy typ wpisu
    if( ! in_array( $typenow, array( 'post', 'page' ) ) )
        return;
	// sprawdzamy czy user ma włączony edytor WYSIWYG
	if ( get_user_option('rich_editing') == 'true') {
		add_filter("mce_external_plugins", "dziudek_add_tinymce_plugin");
		add_filter('mce_buttons', 'dziudek_register_my_tc_button');
	}
}

Jak widać powyższa funkcja dokonuje kilku istotnych operacji – przede wszystkim sprawdza uprawnienia użytkownika i jego ustawienia. Jeżeli wszystkie warunki są spełnione, następuje dodanie dwóch filtrów: dziudek_add_tinymce_plugin oraz dziudek_register_my_tc_button.

Pierwszy z nich służy do określenia ścieżki do skryptu z naszą wtyczką dla TinyMCE:

function dziudek_add_tinymce_plugin($plugin_array) {
   	$plugin_array['dziudek_tc_button'] = ŚCIEŻKA_DO_PLIKU; // np. plugins_url( '/button.js', __FILE__ );
   	return $plugin_array;
}

drugi natomiast służy do dodawania przycisków w edytorze – w tym wypadku dodamy jeden przycisk:

function dziudek_register_my_tc_button($buttons) {
   array_push($buttons, "dziudek_tc_button");
   return $buttons;
}

Przycisk z etykietą tekstową

Teraz możemy przejść do właściwego kodu odpowiedzialnego za dodawanie przycisku do edytora – poniższy kod umieszczamy w pliku *.js podanym w filtrze dziudek_add_tinymce_plugin. Kod ten dodaje przycisk, który po kliknięciu wstawi do edytora tekst Hello World!:

(function() {
    tinymce.PluginManager.add('dziudek_tc_button', function( editor, url ) {
        editor.addButton( 'dziudek_tc_button', {
            text: 'My test button',
            icon: false,
            onclick: function() {
                editor.insertContent('Hello World!');
            }
        });
    });
})();

Może nie wygląda to zbyt oszałamiająco, ale nasz pierwszy przycisk dodany do edytora TinyMCE działa:

btn1

Przycisk ze standardową ikonką

Pora zmienić wygląd naszego przycisku i dostosować go bardziej do wyglądu kokpitu. W tym celu wykorzystamy Dashicons – czyli zestaw ikonek w postaci fonta wykorzystywany w kokpicie.

Zamieniamy nasz kod na:

(function() {
    tinymce.PluginManager.add('dziudek_tc_button', function( editor, url ) {
        editor.addButton( 'dziudek_tc_button', {
            title: 'My test button',
            icon: 'wp_code',
            onclick: function() {
                editor.insertContent('Hello World!');
            }
        });
    });
})();

Zmieniły się dwie rzeczy:

  1. Właściwość text zamieniona została na właściwość title, dzięki temu zniknie tekst z przycisku i pojawi się on w tooltipie po najechaniu na niego.
  2. Zdefiniowaliśmy nazwę wykorzystywanej ikonki. Niestety z moich obserwacji wynika, że bez dodatkowego kodu CSS jesteśmy ograniczeni do wybranych ikonek.

Efekt jest następujący:

btn2

Przycisk z niestandardową ikonką pochodzącą z Dashicons

No dobrze, przyjmijmy, że upraliśmy się na ikonkę z zestawu Dashicons, która nie jest zdefiniowana w kodzie edytora – przykładowo ikonkę z logo WordPressa.

Na szczęście rozwiązanie jest dość proste – musimy dołączyć dodatkowy kod CSS następującej postaci:

i.mce-i-icon {
font: 400 20px/1 dashicons;
padding: 0;
vertical-align: top;
speak: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin-left: -2px;
padding-right: 2px
}

Możemy to zrobić najprościej w ten sposób:

add_action('admin_enqueue_scripts', 'dziudek_tc_css');

function dziudek_tc_css() {
	wp_enqueue_style('dziudek-tc', plugins_url('/style.css', __FILE__));
}

Powyższy kod sprawi, że możemy wykorzystać dowolną klasę postaci dashicons-*. Dla dociekliwych – musieliśmy określić klasę .mce-i-icon, ponieważ wartość właściwości icon jest automatycznie dołączana do ciągu znaków mce-i-. Zmieniamy kod JavaScript na następujący:

(function() {
    tinymce.PluginManager.add('dziudek_tc_button', function( editor, url ) {
        editor.addButton( 'dziudek_tc_button', {
            title: 'My test button',
            icon: 'icon dashicons-wordpress-alt',
            onclick: function() {
                editor.insertContent('Hello World!');
            }
        });
    });
})();

Jak widać jako pierwszą klasę podajemy icon (aby uzyskać klasę mce-i-icon), a potem dodajemy klasę dashicons-wordpress-alt.

Nasz przycisk wygląda teraz następująco:

btn3

Lista klas z zestawu Dashicons dostępna jest na stronie z listą ikon – wystarczy kliknąć daną ikonkę by pojawiła się u góry powiązana z nią klasa CSS.

Własna grafika jako ikonka przycisku

OK, może się zdarzyć, że nawet Dashicons nie spełnią naszych wyszukanych potrzeb w kwestii wyglądu ikonki. Wtedy pozostaje nam stworzenie własnej grafiki – najlepiej 2-3 razy większej niż rozmiar przycisku (ja stworzyłem grafikę w rozmiarach 64x64px, która wygląda ładnie na wyświetlaczach retina) i zdefiniować ją jako tło przycisku w naszym pliku CSS:

i.dziudek-own-icon {
	background-image: url('custom-icon.png');
}

Kod JavaScript zmieniamy na następujący, aby wykorzystać nową klasę:

(function() {
    tinymce.PluginManager.add('dziudek_tc_button', function( editor, url ) {
        editor.addButton( 'dziudek_tc_button', {
            title: 'My test button',
            icon: 'icon dziudek-own-icon',
            onclick: function() {
                editor.insertContent('Hello World!');
            }
        });
    });
})();

Dzięki temu nasz przycisku korzysta z naszej wymarzonej, niestandardowej ikonki:

btn4

Dodajemy przyciskowi submenu

Nasz przycisk prezentuje się już świetnie, ale wciąż jest ograniczony do jednej funkcjonalności. Na szczęście łatwo można to zmienić, musimy wprowadzić do naszego kodu parę zmian:

(function() {
    tinymce.PluginManager.add('dziudek_tc_button', function( editor, url ) {
        editor.addButton( 'dziudek_tc_button', {
            title: 'My test button',
            type: 'menubutton',
            icon: 'icon dziudek-own-icon',
            menu: [
            	{
            		text: 'Menu item I',
            		value: 'Text from menu item I',
            		onclick: function() {
            			editor.insertContent(this.value());
            		}
           	}
           ]
        });
    });
})();

Po pierwsze zmieniliśmy typ przycisku na menubutton, dzięki temu możemy skorzystać z właściwości menu, która zawiera tablicę pozycji submenu. Jak widać pozycja submenu zawiera tekst oraz wartość tekstową wstawianą po kliknięciu (dzięki funkcji zdefiniowanej we właściwości onclick).

Nasz przycisk zmienił też trochę swoje wymiary:

btn5

Dodajemy też sub-submenu 😉

Struktura definowana we właściwości menu może być zagnieżdżana, wystarczy określić w danej pozycji menu właściwość menu:

(function() {
    tinymce.PluginManager.add('dziudek_tc_button', function( editor, url ) {
        editor.addButton( 'dziudek_tc_button', {
            title: 'My test button',
            type: 'menubutton',
            icon: 'icon dziudek-own-icon',
            menu: [
                {
                    text: 'Menu item I',
                    value: 'Text from menu item I',
                    onclick: function() {
                        editor.insertContent(this.value());
                    }
                },
                {
                    text: 'Menu item II',
                    value: 'Text from menu item II',
                    onclick: function() {
                        editor.insertContent(this.value());
                    }
                    menu: [
                        {
                            text: 'First submenu item',
                            value: 'Text from sub sub menu',
                            onclick: function() {
                                editor.insertContent(this.value());
                            }
                        },
                        {
                            text: 'Second submenu item',
                            value: 'Text from sub sub menu',
                            onclick: function() {
                                editor.insertContent(this.value());
                            }
                        }
                    ]
                },
                {
                    text: 'Menu item III',
                    value: 'Text from menu item III',
                    onclick: function() {
                        editor.insertContent(this.value());
                    }
                }
           ]
        });
    });
})();

Nasz przycisk posiada już teraz całkiem rozbudowane menu:

btn6

Niestety szybko zauważymy mały problem – jeżeli klikniemy np. pozycję „First submenu item” to w naszym edytorze pojawi się:

btn7

Krótko mówiąc – naciśnięcie pozycji sub submenu powoduje wywołanie akcji onclick także w nadrzędnej pozycji menu. Na szczęście można ten problem łatwo wyeliminować – wystarczy wykorzystać metodę stopPropagation, która powoduje, że dane zdarzenie nie jest przekazywane w górę drzewa DOM:

onclick: function(e) {
    e.stopPropagation();
    editor.insertContent(this.value());
}

Po takiej zmianie nasze pozycje sub submenu powinny prawidłowo reagować na kliknięcia.

Dodajemy popup po kliknięciu

Do tej pory ograniczaliśmy się do wstawiania określonych wartości tekstowych do edytora, nie zawsze jednak będzie to dla nas wystarczające rozwiązanie. Zróbmy zatem coś co sprawi, że użytkownik będzie mógł określić niektóre wartości dodawanego tekstu – wykorzystajmy popup pojawiający się po kliknięciu na przycisk:

onclick: function() {
    editor.windowManager.open( {
         title: 'Insert h3 tag',
         body: [{
            type: 'textbox',
            name: 'title',
            label: 'Your title'
         }],
         onsubmit: function( e ) {
             editor.insertContent( '<h3>' + e.data.title + '</h3>');
         }
     });
}

Powyższy kod spowoduje wyświetlenie popupa z jednym polem na wprowadzenie zawartości znacznika h3:

btn8

Jak widać właściwość onsubmit spowoduje dodanie do edytora tekstu po zatwierdzeniu zmian przez użytkownika.

Rozbudowywujemy popup

Możemy też oczywiście tworzyć bardziej rozbudowane popupy:

btn9

Powyższy popup korzysta z dwóch pól typu textbox oraz jednego typu listbox:

onclick: function() {
    editor.windowManager.open({
        title: 'Insert h4 tag',
        body: [{
            type: 'textbox',
            name: 'title',
            label: 'Your title'
        },
        {
            type: 'textbox',
            name: 'id',
            label: 'Header anchor'
        },
        {
            type: 'listbox',
            name: 'level',
            label: 'Header level',
            'values': [
                {text: '<h3>', value: '3'},
                {text: '<h4>', value: '4'},
                {text: '<h5>', value: '5'},
                {text: '<h6>', value: '6'}
            ]
        }
    ],
    onsubmit: function( e ) {
        editor.insertContent( '<h' + e.data.level + ' id="' + e.data.id + '">' + e.data.title + '');
    }
});
  • Marcin Bobowski

    Dzieki, na pewno sie przyda 🙂

  • Właśnie wczoraj czegoś takiego szukałam! Dzięki za wpis! Znacząco ułatwiasz mi życie zbierając to wszystko w jednym miejscu :))

    • Dziudek

      Nie będę ukrywał, że wpis powstał głównie dlatego, że w sieci brak tych informacji w jednym miejscu 😉 A niestety dokumentacja TinyMCE jest moim zdaniem kiepska.

  • Mariusz Szatkowski

    oszczędziłeś dzień mojej roboty! dzięki!

  • Paweł Knapek

    Dzięki Ci serdeczne dobry człowieku 😉 Ten wpis oszczędził mi sporo czasu przy przepisywaniu wtyczki.

  • efzet

    Takie Pytanie mam, czy na wersji wordpress’a 3.8.1 też to działa ?

    • Dziudek

      Nie, TinyMCE w wersji 4.* został wprowadzony w WordPressie 3.9.

  • Patryk Maciej Gielo

    Mam pytanie odnośnie rodzajów pól, chciałbym mieć pole w stylu textarea, niestety przeszukując API TinyMCE 4 nie znalazłem nic podobnego. Czy można dowiedzieć się gdzie mogę znaleźć wszystkie wartości body przycisku w tym przypadku.

    P.S Troszkę brakuję mi w artykułach krótkiego odnośnika do stron(y) gdzie taki przykład można zobaczyć.

    • Dziudek

      Wydaje mi się, że w tym wypadku pomogłoby pole typu TextBox z włączoną opcją multiline: http://www.tinymce.com/wiki.php/api4:class.tinymce.ui.TextBox

      Całą listę kontrolek można znaleźć tutaj: http://www.tinymce.com/wiki.php/api4:namespace.tinymce.ui aczkolwiek z tego co już zaobserwowałem nie wszystkie zachowują się poprawnie w WordPressie lub wymagają masy dodatkowego kodu jak np. ColorButton.

      Niestety nie jestem w stanie udostępnić przykładów live, gdyż musiałbym dać dostęp do panelu administracyjnego – dlatego zamiast tego udostępniłem repozytoria na githubie, gdzie można pobrać wtyczki i je sobie samemu zainstalować i potestować różne wypadki zmieniając jedną linijkę kodu (odpowiadającą za wczytanie odpowiedniego pliku).