Magento: Eigenes TinyMCE Plugin aktivieren

Magento bringt mit TinyMCE von Haus aus einen Rich Text Editor mit, der im Backend für Attribute, CMS-Seiten u.a. verwendet wird. Die Integration erfolgt über diverse Adapterklassen und Skripte, die quer über den Magento-Core verteilt sind, darunter das Formular-Element Varien_Data_Form_Element_Editor, ein paar Models im CMS-Modul und diverse Blocks und Helpers im Adminhtml-Modul sowie eigene JavaScripts in js/mage/adminhtml/wysiwyg.

Ich habe nun für meine letzte Extension ein eigenes TinyMCE Plugin entwickelt und stand vor dem Problem, wie ich es aktivieren könnte. Ein einfaches einbinden des Plugin-Skripts schlug fehl, da der TinyMCE Editor vom Magento-Adapter dynamisch nachgeladen wird. Leider habe ich dafür auch nach langem Suchen keine brauchbare Dokumentation gefunden und da Replacements und Hacks von Core-Dateien nicht in Frage kommen blieb mir letztlich nichts übrig als die oben genannten Klassen und Skripte zu analysieren. Als Resultat gibt es hier nun eine Anleitung zum einbinden von eigenen Plugins:

Wysiwyg Konfiguration

Die Konfiguration für den Editor wird zentral vom ‘cms/wysiwyg_config’ Singleton geladen (auch außerhalb des CMS-Moduls!). Dort ist leider sehr viel hart kodiert und es gibt z.B. keine Konfigurationsquelle für Plugins. Dafür wird dort allerdings ein Event getriggert so dass man die Konfiguration mittels Observer erweitern kann. Das Event dazu heißt cms_wysiwyg_config_prepare und hat als Parameter ein Varien_Object namens config.

Ein Observer dafür könnte so aussehen:

class SSE_Example_Model_Observer
{
	public function onCmsWysiwygConfigPrepare(Varien_Event_Observer $observer)
	{
		$config = $observer->getEvent()->getConfig();
		$this->addPlugin($config, $this->getContentBlockPluginConfig());
	}

	private function addPlugin(Varien_Object $config, $plugin)
	{
		$config->setData('plugins', array_merge((array) $config['plugins'], array($plugin)));
	}
	
	private function getContentBlockPluginConfig()
	{
		return array(
			'options' => array(
			),
			'name' => 'example',
			'src' => Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_JS) . 'sse/example/editor_plugin.js',
		);
	}

In der Methode getContentBlockPluginConfig() wird hier die Konfiguration für ein neues Plugin angelegt:

options
Das Options-Array wird einer zusätzlichen Plugin-Schnittstelle von Magento verarbeitet. Hierüber lassen sich Plugins wie der “Insert Image”-Button realisieren, die außerhalb von TinyMCE funktionieren. Dies soll uns an dieser Stelle aber nicht interessieren. 1
name
Der Name des Plugins, so wie er im Plugin-Skript als Parameter für tinymce.PluginManager.add() angegeben wurde
src
Der Pfad zum Plugin-Skript. Dieser kann relativ zum Ort von TinyMCE (tinymce.baseUrl) sein, ich empfehle allerdings der Übersichtlichkeit zuliebe wie oben einen absoluten Pfad zu nutzen und das Plugin in einem eigenen Verzeichnis abzulegen. Da das Plugin von Magento explizit über diesen Pfad geladen wird, sind die Namenskonventionen von TinyMCE hier nicht von Belang, für andere Ressourcen gelten sie weiterhin (Die Sprachdatei wäre hier z.B. in js/sse/example/langs/en.js)
name und src werden clientseitig von der Setup-Klasse in js\mage\adminhtml\wysiwyg\tiny_mce\setup.js verarbeitet, die für das Laden des Editors zuständig ist.

Einfügen des Buttons/Steuerelements ins TinyMCE Menü

Üblicherweise muss man neue Steuerelemente über die theme_advanced_buttons{n} Parameter beim Initialisieren von TinyMCE im Menü einfügen. Da diese Initialisierung aber wie gesagt komplett von Magento übernommen wird, gibt es hier einfach die Konvention dass das Element genauso heißt wie das Plugin (Magento fügt den Namen an vorderster Position in der ersten Reihe ein):

	// ...
	tinymce.create('tinymce.plugins.Example', {
		init : function(editor, url) {
			// ...
			editor.addButton('example', {
				// ...
			});
		},
	// ...
	}

Aktivierung nur für bestimmte Formulare

Dadurch dass die Konfiguration zentral über ein Singleton geladen wird, ist sie leider für jede Editor-Instanz die selbe. In meinem Fall brauchte ich das Plugin jedoch nur für die Produktbeschreibung und wollte es entsprechend nicht in anderen Formularen aktiviert haben. Das ließ sich letztendlich am besten clientseitig bewerkstelligen, indem bei der Plugin-Initialisierung die ID des Editors abgefragt wird (Die ID lässt sich am besten über den DOM-Inspektor von Firebug oder einem ähnlichen Tool herausfinden):

	// ...
	tinymce.create('tinymce.plugins.Example', {
		init : function(editor, url) {
			if (editor.id == "description_editor") {
				this._init(ed, url);
			}
		},
		_init : function(editor, url) {
			// real initialization here
		},
	// ...
	}

Fazit

Wie so oft bei Magento: Eigentlich ganz einfach, wenn man nur weiß wie es geht. Ich hoffe, dem ein oder anderen hiermit das stundenlange Wühlen im Core zu ersparen was ich dafür auf mich nehmen musste 🙂

Notes:

  1. Zum Vertiefen empfehle ich den Quelltext von Varien_Data_Form_Element_Editor::_getPluginButtonsHtml() als Einstiegspunkt.

Magento: Neue Kundenattribute im Backend-Fomular sichtbar machen

Wer in einem Setup-Skript mittels addAttribute() der Customer-Enitität neue Attribute hinzufügen will, erlebt ab Magento 1.5 eine Überraschung. Das Setup läuft zwar fehlerfrei und die DB-Einträge in customer_eav_attribute sind anschließend vorhanden aber es findet sich im Backend kein neues Formular-Element.

Was mit anderen Entitäten wie z.B. Kategorien problemlos funktioniert und bis Magento 1.4 auch mit Kunden, benötigt hier nun eine zusätzlichen Setup-Schritt mit dem das Attribut explizit dem Admin-Formular hinzugefügt wird.

Dazu existiert eine neue Tabelle customer_form_attribute, die für jedes Attribut festlegt, in welchen Formularen es verwendet wird:

mysql> select * from customer_form_attribute;
+----------------------------+--------------+
| form_code                  | attribute_id |
+----------------------------+--------------+
| adminhtml_customer         |            1 |
| adminhtml_customer         |            3 |
| adminhtml_customer         |            4 |
| checkout_register          |            4 |
| customer_account_create    |            4 |
| customer_account_edit      |            4 |
| adminhtml_customer         |            5 |
| checkout_register          |            5 |
| customer_account_create    |            5 |
| customer_account_edit      |            5 |
| adminhtml_customer         |            6 |
| checkout_register          |            6 |
| customer_account_create    |            6 |
| customer_account_edit      |            6 |
| adminhtml_customer         |            7 |
| checkout_register          |            7 |
| customer_account_create    |            7 |
| customer_account_edit      |            7 |
[...]
+----------------------------+--------------+
88 rows in set (0.00 sec)

Aber dies nur zum Verständnis, natürlich muss man nicht selber an der Datenbank Hand anlegen, es genügen folgende Zeilen nach dem addAttribute()-Aufruf im Setup-Skript:

    Mage::getSingleton('eav/config')
        ->getAttribute('customer', $attributeCode)
        ->setData('used_in_forms', array('adminhtml_customer'))
        ->save();

wobei $attributeCode der eindeutige Code des neuen Attributs ist und 'adminhtml_customer' der Code für das Kundenverwaltungsformular im Backend. Um die Setup-Skripte übersichtlicher zu halten empfiehlt sich eine Erweiterung der Entity Setup-Klasse etwa wie folgt:

class MyNamespace_ExtendableCustomer_Model_Entity_Setup
    extends Mage_Customer_Model_Entity_Setup
{
    public function addAttributeToForm($attributeCode, $formCode)
    {
    	Mage::getSingleton('eav/config')
            ->getAttribute('customer', $attributeCode)
            ->setData('used_in_forms', (array) $formCode)
            ->save();
    }
}

Nutzt man nun diese Klasse für seine Setup-Skripte, die Kundenattribute hinzufügen, ist es ganz einfach:

$this->addAttribute('customer', 'neues_attribut', array( /* ... */);
$this->addAttributeToForm('neues_attribut', 'adminhtml_customer');

Analog kann das Attribut auch zu anderen Formularen hinzugefügt werden (Codes siehe oben).

Danke ans Magento-Forum für den entscheidenden Hinweis!

Update

Achtung, beim Speichern wird zusätzlich das Feld eav_entity_attribute.sort_order gesetzt, unabhängig davon ob dieses schon vorher spezifiziert wurde!

Siehe dazu mein Kommentar auf StackOverflow.

Weiterhin wirkt sich sort_order nur aus wenn beim Anlegen des Attributs user_defined => 0 gesetzt wird, da vor sort_order nach user_defined sortiert wird!