Hello,

I'm would like to write/ port an editor integration (jquery based editor) but don't know how the interface works. Is a documentation available which describes how the functions work an what the code do (which format is needed for plugins? have to converted before return data? ...)?

Best regards

Comments

TwoD’s picture

Status: Active » Needs work

We haven't gotten around to making an official documentation of this yet. There's been little interest earlier and we've simply focused on other things. I wrote a little about this not long ago in another issue, but that could be a bit hard to find so I'll write about it here as well. Maybe this could be the start of such a documentation addition to wysiwyg.api.php and wyswyg.api.js where our plugin hooks are documented?

There are two ways to create an editor implementation. Or rather, there are two ways to make Wysiwyg core recognize and use it. In both cases you need one editorname.inc (PHP) file and at least one editorname.js (JavaScript) file, for the serverside and clientside implementations. We'll get to the actual contents of the file in a moment. The .inc file's purpose is to provide meta data about the editor library like what JavaScript files it uses, what buttons/plugins/extension it provides, what the currently installed version is and if there are any version specific overrides and some other stuff. The .js file does the heavy lifting when it comes to actually instancing and using the editor on a field, it also takes care of cleaning up anything added by the editor when it is destroyed.

The first way to get the editor implementation recognized is simple, just put the .inc file at wysiwyg/editors/editorname.inc and the JS file in wysiwyg/editors/js/editorname.js.
Wysiwyg loads all .inc files in the editors folder and will be instructed to add the .js file when it's time to use the editor.

The second way is to provide an equivalent of the editors folder, but in a different module. This is done by implementing hook_wysiwyg_include_directory($type). You'll probably notice that this is the same hook used for providing cross-editor "Drupal plugins", but now we're interested in returning 'editors' (or whatever you name the module's subfolder) when $type is 'editors'. There's a short sample implementation in wyswyg.api.php. This simply tells Wysiwyg to also look for .inc file in sites/[all|sitename]/modules/MYMODULE/RETURNED-STRING when listing what editors are available on admin/settings/wysiwyg, or when being told to load a certain editor library. Let's say it finds myeditor.inc in there. Then it'll call a hook implentation to get more info about 'myeditor': MYMODULE_myeditor_editor().

We'll use the TinyMCE implementation as a reference here:

function wysiwyg_tinymce_editor() { // NOTE: TinyMCE's provided by Wysiwyg module itself, hence MYMODULE=wysiwyg. This is in tinymce.inc, hence myeditor=tinymce.

 /* Editor metadata consists of a large array which define some static data and the names of a bunch of callback functions to get dynamic data. We return an array of these massive arrays using the library name as the key. */ 

  $editor['tinymce'] = array(
/* This is what's visible to the user whenever the editor is mentioned.*/
    'title' => 'TinyMCE', 
    'vendor url' => 'http://tinymce.moxiecode.com',
    'download url' => 'http://tinymce.moxiecode.com/download.php',

/* Below we'll tell Wysiwyg module what base path we'd like to use when loading the library's files. The helper function wysiwyg_get_path() will return something like "sites/all/libraries/tinymce", depending on where the editor library was installed. ( We'll use Libraries API module later, and it may allow custom paths as well.)*/
    'library path' => wysiwyg_get_path('tinymce') . '/jscripts/tiny_mce',

/* Next we'll need to inform Wysiwyg about the variants this library comes in. Empty key simply means "the regular/non-special-purpose variant". */
    'libraries' => array(
      '' => array(
        'title' => 'Minified',
/* If the JavaScript file set here doesn't behave well when 'Optimize JavaScript' is on we can turn this into 'files' => array('tiny_mce.js' => array('preprocess' => FALSE)) to have it skipped during aggregation.*/
        'files' => array('tiny_mce.js'),
      ),
/* TinyMCE also has a "Source" variant which loads all the nicely formatted files instead. Only the first variant, which is considered the default selection, is used today but we list all available variants for future use.  */
      'src' => array(
        'title' => 'Source',
        'files' => array('tiny_mce_src.js'),
      ),
    ),
/* This value is the name of a function which will return the currently installed editor library version, usually determined by parsing one of its files. */
    'version callback' => 'wysiwyg_tinymce_version',
/* Here we expect to be returned a list of the available themes (or skins like CKEditor does), often just a static list since we don't fully support this yet but it could also do some file parsing or folder detection. */
    'themes callback' => 'wysiwyg_tinymce_themes',
/* This function is a bit special. It's responsible for converting the editor profile settings stored in the {wysiwyg} table into a JavaScript Array/Object which can be used to create an editor instance. Its return value is expected to be a PHP array which will get converted to JSON by drupal_add_js() and put in Drupal.settings.wysiwyg.configs.editorname.format# on the client. */
    'settings callback' => 'wysiwyg_tinymce_settings',

/* This callback should return an array where the keys are [internal] names of plugins used by the editor. The values are also arrays, holding meta data about where the plugin is located, what buttons it provides and if it's bundled with the editor by default ('internal' key is TRUE, all plugins defined here should have this set.). This would actually be the same array returned from an implementation of hook_wysiwyg_plugins() as documented in wysiwyg.api.php, with the difference that buttons not provided by actual plugins are placed under a key named 'default'. */
    'plugin callback' => 'wysiwyg_tinymce_plugins',
/* This one returns a list of files to load based on the enabled plugins. Used by the editor's .js file if some [external] plugin files are not loaded by the library itself. */
    'plugin settings callback' => 'wysiwyg_tinymce_plugin_settings',
/* This entry is used if your .js implementation can handle cross-editor plugins. The 'drupal' key defines the name of a "plugin" which will hold all the buttons provided by the cross-editor plugin. Most editors don't use this entry yet and explaining it in detail would take some time. */
    'proxy plugin' => array(
      'drupal' => array(
        'load' => TRUE,
        'proxy' => TRUE,
      ),
    ),
/* All cross-editor plugin definitions are passed to this callback so the implementation can add any extra data the editor needs, or convert things to a format it can handle better. */
    'proxy plugin settings callback' => 'wysiwyg_tinymce_proxy_plugin_settings',
/* Each key in this array is the minimum library version supported by the corresponding clientside .js implementation files. The highest keyed array which is not above the installed library version is the only one ever used. The entries below informs Wysiwyg it needs to load js/tinymce-2.js if an old library version is installed and tinymce-3.js if a newer version is used. Subkeys defined here overrides the values set above, allowing newer (or older) versions to use different download urls, load other clientside implementations and even provide different library variants. */
    'versions' => array(
      '2.1' => array(
        'js files' => array('tinymce-2.js'),
        'css files' => array('tinymce-2.css'),
        'download url' => 'http://sourceforge.net/project/showfiles.php?group_id=103281&package_id=111430&release_id=557383',
      ),
      // @todo Starting from 3.3, tiny_mce.js may support JS aggregation.
      '3.1' => array(
        'js files' => array('tinymce-3.js'),
        'css files' => array('tinymce-3.css'),
        'libraries' => array(
          '' => array(
            'title' => 'Minified',
            'files' => array(
              'tiny_mce.js' => array('preprocess' => FALSE),
            ),
          ),
          'jquery' => array(
            'title' => 'jQuery',
            'files' => array('tiny_mce_jquery.js'),
          ),
          'src' => array(
            'title' => 'Source',
            'files' => array('tiny_mce_src.js'),
          ),
        ),
      ),
    ),
  );
  return $editor;
}

The settings callback function for some editors can look very intimidating but it's not really that complex. It basically gets an array with all the values from the profile config form at admin/settings/wysiwyg/profile/#/edit as the $config argument. These are 'translated' to keys and values the editor can understand and set on the $settings array, but not all of them are currently useful for all editors (I'm working on customizing that form for each editor). The $settings array is returned and later passed via drupal_add_js() to become Drupal.settings.wysiwyg.config.editorname.format# (where # corresponds to the format id the profile is associated with) on the client.
There's a special setting called 'buttons', which of course is where all the buttons you selected from under "Buttons and Plugins" are listed, keyed by the plugin providing them. This one usually takes a bit of effort to translate as it's also used to figure out what plugins need to be active. Some plugins may not even have buttons, so we just call them extensions.

The JavaScript implementation is currently a bit tricky and I'm running out of time here so this will be really brief.
There are a couple of methods that each editor implementation must define.
The first one is "attach" (set as Drupal.wysiwyg.editor.attach.editorname). It gets the $settings array passed to it (now as a JavaScript array) whenever an editor is to be attached to a field. It also gets some field-specific data in the param argument, to say if the editor should be active at all, what the id of the field is, and what the selected input format is (this is only called for an editor when a profile of its type is associated with the selected format, so there's no danger of attaching the editor at the wrong time). The context is an element containing the original textarea (and now the editor instance too), useful for passing as the context argument to jQuery()/$().

The second one is "detach", which is obviously the reverse of the first method. Destroy the editor instance - don't forget to sync the contents back to the original field first, if the editor doesn't do this on its own - then clean up any event handlers that editor has set on the form or elsewhere. A "leave it as you found it" approach is used here. ;) You never really know why the editor was detached. The form could have been submitted, the editor might just have been deactivated or is about to be replaced by another editor.

The third one's a special method which may not be needed by all editors. It's Drupal.wysiwyg.editor.init.editorname and it's called during page load, before the onload events have fired. TinyMCE needs to run at this point to set some global library data and load external plugin files, otherwise they won't be available if a TinyMCE instance is active by default.

Next comes Drupal.wysiwyg.editor.instance.editorname, which is currently a great source of confusion. It's meant to be a collection of methods which act directly on an attached editor instance, called only when an instance of the editor in question is attached. You'll see that we sometimes call methods set here directly, though I think that's wrong since Wysiwyg core only uses this object as a template for creating actual instance objects in Drupal.wysiwyg.instances[field-id] before the editor is attached. Note: Not actual TinyMCE/CKEditor/FCKeditor instances, "instances" of our implementation of an editor instance. Get it? ;)
Anyway, there's really only one method you currently need to care about on the instance object: "insert". This is called when it's time to, well, insert a HTML snippet in the editor. Both cross-editor plugins and other modules can call this method, so it's a bit har to know what to expect as input. If the editor has an "insert html" API function, this is the time to call it. Note: "this" inside that method will refer to Drupal.wyswyg.instances[field-id] as it would be called like: Drupal.wyswyg.instances[field-id].insert('<h1>Hello World!</h1>').
The other methods in the instance object are editor-specific and not needed by all editors. The open/closeDialog methods were meant to provide a way for cross-editor plugins to "blend in" a bit more and use the editor's own dialog templates etc, but it turned out to be a mess to implement this way. D7 has dialog support anyway and we'll probably backport a similar solution to D6 if we don't just cut it all out. (AFAIK, only img_assist relies/relied on it, and it just works/worked with TinyMCE much for this reason.)

Inside the insert method on the instance, you'll have access to all the parameters from the attach function as properties of the "this" reference. this.field gives the id of the current field for instance. This is also true for any other method on the instance object given they were called via Drupal.wysiwyg.instances[field-id].method() and not directly via Drupal.wysiwyg.editor.instance.editorname.method().

I hope that helps some. To determine what all the arguments in the PHP callbacks contain, I can recommend using the Drupal for Firebug module and its firep() function. (Similar to Devel's dsm() or print_r() but you see the output in a Firebug tab.)

sun’s picture

We want to move most of this into wysiwyg.api.php :)

JohnnyX’s picture

Ok, thanks. I will take a look and do a first try to integrate cleditor. My first steps with drupal modules :)

JohnnyX’s picture

Please delete...

JohnnyX’s picture

Problem 1:
"The installed version 1.2.5 of clEditor is not supported."

I don't know why isn't supported.

versions

'versions' => array(
'1.2.5' => array(
'js files' => array('jquery.cleditor.js'),
'css files' => array('jquery.cleditor.css'),
),

wysiwyg_cleditor_version()

function wysiwyg_cleditor_version($editor) {
$script = $editor['library path'] . '/jquery.cleditor.js';
if (!file_exists($script)) {
return;
}
$script = fopen($script, 'r');
fgets($script);
$line = fgets($script);
if (preg_match('|@preserve CLEditor WYSIWYG HTML Editor v([0-9\.].*)$|', $line, $version)) {
fclose($script);
return $version[1];
}
fclose($script);
}

It returns 1.2.5! Tested with a simple echo $version[1]; break; But doesn't work... If I add return '1.2.5'; to first line of function it works...

Problem2:
Editor will not appear after attached.

Drupal.wysiwyg.editor.attach.cleditor = function(context, params, settings) {
$('#' + params.field).cleditor();
};

.cleditor() should simply show an editor with toolbar. Maybe a bug with Drupal 7 (jquery) or wysiwyg module?

TwoD’s picture

Your regular expression might be catching an extra character after the version string, such a s a newline. Try with just if (preg_match('|@preserve CLEditor WYSIWYG HTML Editor v([0-9\.]*)|', $line, $version)) {. Or perhaps if (preg_match('|@preserve CLEditor WYSIWYG HTML Editor v([0-9]+(?:\.[0-9]+)*)|', $line, $version)) { to find that string followed by at least one number then optionally followed by multiples of a dot followed by at least one number?

I'm not sure why the editor wouldn't attach, the code looks ok. Are you getting any errors? Have you tried using Firebug to insert a breakpoint on the .cleditor() row and stepped from there?

JohnnyX’s picture

TwoD:
You're right, thanks! Maybe a extra charakter after version string. Fixed and works :)

But attach editor is strange... I can't find any wysiwyg or cleditor string in source code?! No path or file found.

TwoD’s picture

Could you post/attach the entire files?

sun’s picture

Title: Documentation how to integrate a new editor to wysiwyg api module? » How to integrate a new editor
Kate101’s picture

Hello,

I'm seeing the same issue as described by JohnnyX (above) when I'm trying to integrate a new editor:

Problem2:
Editor will not appear after attached.

Drupal.wysiwyg.editor.attach.cleditor = function(context, params, settings) {
$('#' + params.field).cleditor();
};

Did you ever manage to resolve this? I've tried debugging but am seeing no errors thrown.

Thank you!

TwoD’s picture

@Kate101, is the script file containing that snippet being included in the document by Wysiwyg when a format with that editor is available?
Have you set a breakpoint in the attach method to see if it gets called?

Could you post the entire integration files (prefereably the entire module implementing the Wysiwyg hooks) you are using? It's a bit difficult to guess what's wrong without having any code to look at.