The current work in progress in #245799: Update TinyMCE integration to 3.x uncovered some caveats Wysiwyg API needs to handle. Specifically:

  • APIs of editor libraries can change. All Drupal modules that integrate with a particular editor need to update their scripts accordingly, breaking the functionality for users who still use the old version.
  • Contrib modules should not be forced to implement editor-specific code in their modules/output. They need a simple way to output the contents for a (modal) dialog, without having to deal with plenty of different editors and their different ways of fetching/inserting contents from/into a client-side editor.
  • By implementing and loading different JavaScripts for simple plaintext textareas and TinyMCE, Image Assist was actually ahead of its time, back then when this was implemented. Invoking editors via callstacks/hooks is basically what Wysiwyg API does now to initialize, attach, and detach editors from a textarea. We need a similar mechanism for editor plugins now.
  • The concrete culprit of Image Assist is that it ships with one core file, img_assist.js, two integration files, img_assist.textarea.js and img_assist.tinymce.js, as well as an editor plugin file for TinyMCE 2. For plaintext textareas, only img_assist.textarea.js is loaded, which contains the callback functions to insert an Image Assist inline tag from the popup window into the textarea. The other file, as well as the editor plugin, is only loaded when TinyMCE is attached to a textarea. So both integration files do more or less the same, just invoking different functions for a different target.
  • Due to this structure and the fact that Image Assist uses a frameset for its dialog, the underlying system of functions and their dependencies is not easy to grasp. The frameset is also completely unnecessary, so I'll continue as if Image Assist would not use a frameset for its dialog from here.
  • img_assist.tinymce/textarea.js contains 3 functions:
    • init() to initialize some data/contents in the dialog based on passed in parameters,
    • getFilterTag() to generate appropriate output based on the form values in the dialog, either a HTML representation of an inline macro tag for the editor, or a plain inline macro tag for the textarea,
    • insertToEditor() to insert the resulting HTML or inline macro tag into the editor or textarea.
  • editor_plugin.js, the actual editor plugin for TinyMCE 2, contains 6 functions:
    • addCommand() to open the dialog when the editor button is pressed,
    • addButton() to add the editor button,
    • onInit() to load Image Assist's CSS in the dialog,
    • onBeforeGetContent() to replace HTML images with inline macro tags upon detaching the editor,
    • onBeforeSetContent() to replace inline macro tags with HTML images upon attaching the editor,
    • onNodeChange() to mark the editor button as enabled when an image is selected.
  • It is important to know that most client-side editor libraries support almost the same features and structure like the one in editor_plugin.js. Similar to the noted functions, there is also always a function to insert contents into an editor, which means that insertToEditor() could also be located in editor_plugin.js. This would result in 2 (rather private) functions dealing with the dialog, and 7 functions to interact with the editor.
  • It is also important that some of the editor interaction functions just register some properties on the editor instance, and some of them process data supplied via function arguments.
  • We are always dealing with DOM nodes or plain strings that should be inserted or updated in the editor and all editors I have seen so far follow this flow in comparable ways. Since Drupal ships with jQuery built-in, we do not have to use editor-specific library functions to do what we want.
  • So here is the goal:
    • Add a stub/proxy plugin for each editor, containing the above mentioned 7 functions.
    • Alter hook_wysiwyg_plugin() so that each Drupal module is able to register one or more plugins by defining
      • name: Used internally in the plugin registry and for determining callback function names. Probably automatically assigned by the editor plugin API.
      • title: Used mainly on administrative screens.
      • vendor url: Used on administrative screens only.
      • js path/js file: Will be loaded when the proxy plugin initializes, contains above mentioned 7 hooks, but using "wysiwygified" names, such as .init(), .attach(), .detach(), .insert() aso. Those hooks will be invoked by the proxy plugin. Also a .isNode() callback, which is invoked for testing the current selection like in onNodeChange() of above example.
      • icon filepath: Used for the editor button, defaults to js-path/images/plugin-name.png, size defaults to 16x16. Other sizes for some weird editor formats will be searched via filename suffixes.
      • css filepath: Used to load custom CSS files for the dialog.
      • dialog url: The Drupal menu path to invoke for the (initial) dialog screen.
      • dialog properties: An array containing various properties for the dialog, such as width, height, inline, or resizable. Whether they are respected/supported depends on the editor.
      • Also required, but postponed for later: 'plugin settings', 'plugin settings form'. Plugin settings must be able to extend/override the editor settings for a profile.
    • Modules like Image Assist will just output the contents of the dialog. Most often, this is a simple form that configures contents for the editor. But for modules like Image Assist we also need to support multi-page/-step forms. Since not all of the dialog pages contain Insert/Update and Cancel buttons, but those buttons need to invoke the corresponding action/function of the editor, the proxy plugin needs to store a copy of the editor-specific functions in Wysiwyg API's namespace, f.e. Drupal.wysiwyg.plugin.insert(). This way, the JavaScript of the implementing module is able to assign it easily to the appropriate buttons in the dialog.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sun’s picture

First iteration. Pretty close. Only works in FF due to some leftover Firebug debugging code.

Please note that the Drupal/major version of this issue does not matter, since all the code we touch is our own module/API code, which is almost identical for all Drupal versions.

Hm. After reviewing this patch, I guess it would be helpful to commit some of the structural changes first...? (moved editor function callstacks from Drupal.wysiwyg.* to Drupal.wysiwyg.editor.* to make space for our new Drupal.wysiwyg.invoke callstack)

sun’s picture

Status: Active » Needs review
sun’s picture

Committed the namespace changes separately.

sun’s picture

This patch adds the first working editor plugin, provided by a Drupal module.

sun’s picture

Now supporting TinyMCE 2, YAY!

quicksketch’s picture

Updated patch (only for Drupal 6/HEAD version) to include invoke commands for plain textareas ("none.js"). This is rocking!

sun’s picture

Invoke functions for the none editor are properly updated now, when no editor is attached. Hence, doing

Drupal.wysiwyg.invoke.insert('edit-body', 'FOO!');

and disabling the editor (in various ways) and doing

Drupal.wysiwyg.invoke.insert('edit-body', 'FOO!');

results in the expected action -- insert "FOO!" in the textarea/editor/textarea/editor/textarea/editor/textarea/editor/textarea/editor/textarea/editor/... 8)

However, I think we need to pass some context info to plugins provided by Drupal modules, since they would have, for example, to do Drupal.wysiwyg.invoke.insert('edit-body', '<!--break-->'); for the none editor, and Drupal.wysiwyg.invoke.insert('edit-body', '<img class="wysiwyg-break" ... />'); for an HTML editor.
TinyMCE implemented some nice way to support this, speaking JS:

data = {};
data.content = 'Editor content goes here';
data.format = 'html' | 'raw' (= text) | 'xml';

So instead of passing the content as plain string, we should pass it as object including some meta information for Drupal plugins provided via the proxy plugin.

sun’s picture

Talk is silver, code is gold -- Practical example, break.js:

  // Execute the button.
  invoke: function(data, settings, instanceId) {
    if (data.format == 'html') {
      // Prevent duplicating a teaser break.
      if ($(data.node).is('img') && $(data.node).attr('class') != 'wysiwyg-break') {
        return;
      }
      var content = this._getPlaceholder(settings);
    }
    else { // May even check explicitly for data.format == raw|text here
      // @todo Some logic to search for existing tag here and bail out if one exists.
      var content = '<!--break-->';
    }
    Drupal.wysiwyg.invoke.insert(instanceId, content);
  },

So, basically, Drupal.wysiwyg.invoke.insert() is a dumb function. Drupal plugins have to sort out the target format of an inserted/updated content before altering the editor contents. So either we hard-code data.format in the proxy plugin (according to the editor), or might even turn this into a profile/input format setting.

sun’s picture

Attached patch adds the required data.format for Drupal plugins like Teaser break.

Additionally, the function prepareContent() was added to the invoke functions (for both TinyMCE 2 and 3), which parses the content that is about to be inserted and adds special properties to elements, which need them, because most editors apply internal DOM properties to distinguish whether a content element belongs to an internal editor button (for example, selecting the Teaser break image should not highlight TinyMCE's built-in image button).

Drupal plugin implementations now also support the properties "icon title", which moves the localization of buttons completely into Drupal's localization system.

sun’s picture

Before moving ahead and trying to implement Image Assist as new style API plugin, the new API code badly needs a major clean-up. The current PHP part of the implementation is rather a hack. So what happens there:

  1. First an foremost, all of this deals with EXTERNAL editor plugins only. External plugins are located outside of an editor's library and must be loaded separately. Internal editor plugins are located within an editor's library and do not need special handling, besides adding them to a list of plugins to load.
  2. Each editor (version) can define a "proxy plugin" in its editor definition. The internal name of this proxy plugin must be "drupal" and it must have the schema of hook_wysiwyg_plugin().
  3. wysiwyg_editor_add_plugin_settings(): When an editor is loaded, all external editor plugins are collected via hook_wysiwyg_plugin(). After that, "Drupal plugins" (which work through the proxy plugin) are added to the collection. Lastly, only enabled plugins are kept. Culprits here:

    a) The schema and structure of (editor-specific) plugins provided via hook_wysiwyg_plugin() is different than the one of Drupal plugins.

    b) Drupal plugins are stacked into one proxy plugin, internally called "drupal".

    c) The new Drupal plugins can be loaded separately. But the old hook_wysiwyg_plugin() plugins cannot, because the defining module is unknown. A plugin "registry" doesn't make sense in terms of D7, but we should gather the defining module names to store them in a Wysiwyg profile, directly invoke the corresponding functions, and finally improve performance.

    d) Instead of rebuilding the plugin list, this function should probably rely on wysiwyg_editor_get_plugins().

  4. The collection of enabled plugins is passed to the editor specific "plugin settings callback". Like the "settings callback", this callback is intended to add JS settings for editor plugins. We have to do this, since we do not know exactly which information an editor needs about plugins. Culprits here:

    a) Even the simple Teaser break plugin relies on settings.path, resp. Drupal.settings.wysiwyg.plugins.break.path, which means that plugin settings are actually divided into plugin-specific settings and editor-specific settings. The former must not be overridden.

    b) The current plugin settings callback for TinyMCE also adds the JavaScripts of Drupal plugins. But loading external scripts and just adding settings should be clearly separated. (Scripts are required to be loaded in a certain order, and Drupal.settings must come before wysiwyg_editor.js)

    c) The plugin settings callback needs to explicitly check for the plugin name "drupal", and has to add different settings for the Drupal plugins provided via the proxy plugins.

    d) Because of the missing separation between editor-specific plugins and Drupal plugins, there is also an ugly variable type check in Drupal.wysiwyg.editor.init.tinymce().

  5. wysiwyg_editor_get_plugins(): Is used for the administrative profile configuration form. And by TinyMCE's "settings callback" to walk through all available plugins and add the enabled plugins to TinyMCE's editor initialization settings.

    a) Again, a hard-coded check for the internal "drupal" plugin name here.

    b) Probably most confusing at first glance: The "drupal" proxy plugin is NOT added to the editor initialization settings. Only the buttons are added, since the proxy plugin itself adds itself in tinymce-3.js, for example.

    c) If we would not stack Drupal plugins into the 'buttons' array of the proxy plugin, they would not appear on the profile configuration form.

sun’s picture

Committed the code clean-up changes separately.

sun’s picture

Committed another namespace clean-up separately, changing the JS *settings* namespace from 'wysiwygEditor' to 'wysiwyg'; a prerequisite for this patch.

sun’s picture

Attached patch eliminates:

3a
4a,b,c,d

by

  • separating the logic and callbacks for 'native' plugins and 'drupal' plugins (supplied via the proxy plugin)
  • adding a 'proxy plugin settings callback', accompanying the existing 'plugin settings callback', but for Drupal plugins
  • moving Drupal plugin-specific 'settings' into the plugin definition (which will be enhanced by a corresponding 'settings form callback' and 'settings callback' later on; separate issue)
  • adding plugin settings into separate namespaces/locations:

    Drupal.settings.wysiwyg.plugins.tinymce.native: contains native plugins to load.

    Drupal.settings.wysiwyg.plugins.tinymce.drupal: contains an array of Drupal plugin settings to process via the proxy plugin.

    Drupal.settings.wysiwyg.plugins.drupal: contains settings specific for each Drupal plugin (currently, a 1:1 copy of the 'settings' definition of a Drupal plugin).

So, what's left?

3b) Drupal plugins are stacked into one proxy plugin, internally called "drupal".

5a) Again, a hard-coded check for the internal "drupal" plugin name here.

5b) Probably most confusing at first glance: The "drupal" proxy plugin is NOT added to the editor initialization settings. Only the buttons are added, since the proxy plugin itself adds itself in tinymce-3.js, for example.

5c) If we would not stack Drupal plugins into the 'buttons' array of the proxy plugin, they would not appear on the profile configuration form.

The following are rather a performance issues, and I'd like to defer anything related to performance for later:

3c) The new Drupal plugins can be loaded separately. But the old hook_wysiwyg_plugin() plugins cannot, because the defining module is unknown. A plugin "registry" doesn't make sense in terms of D7, but we should gather the defining module names to store them in a Wysiwyg profile, directly invoke the corresponding functions, and finally improve performance.

3d) Instead of rebuilding the plugin list, this function should probably rely on wysiwyg_editor_get_plugins().

sun’s picture

Last patch update for today.

smk-ka’s picture

I do not understand the purpose of this part of Drupal.wysiwygAttach():

    // Provide editor callbacks for plugins.
    Drupal.wysiwyg.invoke = {};
    if (typeof Drupal.wysiwyg.editor.invoke[params.editor] == 'object') {
      Drupal.wysiwyg.invoke = Drupal.wysiwyg.editor.invoke[params.editor];
    }
    // Store this editor id, so (external) plugins can use it.
    Drupal.wysiwyg.activeId = params.field;

Since this function gets executed in a loop, Drupal.wysiwyg.invoke (BTW, using a verb for what's supposed to be an object is a bad choice) and Drupal.wysiwyg.activeId will be overwritten with the last editor instance.

sun’s picture

sun’s picture

Quick braindump after discussing #15 with smk-ka, since I need to work on something different for the next hours:

  • "Drupal.wysiwyg.invoke" is ok, if one knows its purpose. However, we should use the usual way of "Drupal.wysiwyg.subject.verb".
  • Our subject is the currently active editor. TinyMCE uses "activeEditor" for this, which would work out here, too.
  • However, since the current concept doesn't work out at all, we have to store invoke functions for each form element/textarea id. In turn, leading to Drupal.wysiwyg.instances.edit-body[.addPlugin|.insert|...].
  • Instead of updating Drupal.wysiwyg.activeId upon attaching an editor, we need to update this variable when a user focuses or works in an editor instance. TinyMCE provides global callbacks for this purpose (onNodeChange or similar). I'm unsure whether other editors do this, too.
  • "instances" sounds a bit strange, but since we are already passing "instanceId" as argument to Drupal plugins, this name would be conistent.
sun’s picture

sun’s picture

Re-rolled, since frickin' awesome #327100: Associate editors/profiles with input formats has landed. 8)

- Changed Drupal.wysiwyg.invoke into Drupal.wysiwyg.instances, as well as Drupal.wysiwyg.editor.invoke.<editor> into Drupal.wysiwyg.editor.instance.<editor>.

sun’s picture

Re-rolled after a flood of patches went in.

Also contains some overlapping changes for #328052: Switching between input formats conflicts with enable/disable rich-text.

Roi Danton’s picture

A perfect, conceivable workflow for a module that wants to use editors API is written here. Maybe this could be inspiring for the editors API in one way or another.

sun’s picture

Re-rolled after another flood of patches went in.

Ryanbach’s picture

sub

Murz’s picture

subscribe

PixelClever’s picture

subscribing...

Owen Barton’s picture

This looks really promising - is it worth applying and testing the patch, or is there still architectural stuff to be figured out?

sun’s picture

Aside from the points listed in #13, the design is basically fleshed out, I think. The "Teaser break" plugin for core's "<--break-->" teaser splitter inline tag should already work with TinyMCE 3, TinyMCE 2, and the default textarea (well, if you invoke the function to insert it from the console ;).

It also turned out to be good decision to already add support for other editors in the meantime. This revealed that some editors do not support external plugins at all (which would already be covered by the current proxy plugin design), some other editors use a completely different, but very flexible/overengineered way of defining and loading plugins (YUI/markItUp), and some editors do not have built-in support for popups or modal dialogs. The latter is one of the major issues I'm tinkering about, since we have two options for invoking/presenting the UI of Drupal plugins:

  1. Use the editor's built-in functions for displaying popups/modal dialogs, if available.

    Pro: The interface and flow for Drupal plugins (that need an interface) is the same interface as for other, internal plugins of an editor. For example, if TinyMCE is configured to use modal dialogs (the "Inline popups" plugin), Drupal plugins could be presented in the same way as internal plugins by re-using TinyMCE's modal dialog functions. If it is configured to use popup windows, Drupal plugins would use popup windows as well.

    Con: Popup/dialog functions are different for all editors, so we need a very solid proxy plugin API to make the required information available at the right time. If the editor uses popup windows, we need to ensure that jQuery and other scripts are properly loaded in the window. To re-use the UI of internal editor plugins, we would also have to create a "proxy dialog template" for Drupal plugins, which would have to provide "Insert"/"Update"/"Cancel" button functionality.

  2. Use own functions for displaying modal dialogs.

    Pro: No overhead with editor-specific popup/dialog functions. We know that jQuery and plugin scripts are already loaded and available on the page.

    Con: Drupal plugins would have a different UI than internal editor plugins. We either need to implement own library functions for invoking/displaying/resizing/draggin/closing a modal dialog, or we would have to add a dependency on another module - and I'm unsure whether there is one at all (popups? jQuery UI?).

In either case, dialog contents of Drupal plugins are usually presenting forms, so we have to ensure, resp. implement, methods for validating the form values and presenting error messages in the dialog via AJAX.

Re-rolled.

pillarsdotnet’s picture

"

tstoeckler’s picture

Version: 5.x-1.x-dev » 6.x-1.x-dev

The attached patches have been against the D6-dev version for quite a while. Did I miss something or was the Version tag simply outdated?

sun’s picture

Re-rolled against latest HEAD.

Also fixed wrong instanceId passed to Drupal plugins in TinyMCE 2.

sun’s picture

This is a major breakthrough.

Also attached is a patch for Image Assist HEAD, which held off this patch. It's not yet generic, but makes Image Assist compatible to the new plugin API.

To summarize:

- The Teaser break plugin shipped with Wysiwyg API (on behalf of Drupal core) should work both in TinyMCE 3 and TinyMCE 2. It should also work when invoked manually.
- Image Assist should work with TinyMCE 3.x, using Wysiwyg's new plugin API.

sun’s picture

Version: 6.x-1.x-dev » 6.x-2.x-dev
Status: Needs review » Fixed
FileSize
11.7 KB
46.93 KB

LAST MINUTE fixes:

- Due to a wrong execution order, editor instance callbacks received the wrong editor field id ('this.field') of the previously attached editor.

- Fixed a bunch of wrong PHPDoc and coding-style flaws.

Note: All issues related to this patch as well as improvements related to @todo statements in this patch should be treated in separate issues. Thanks in advance.

WELCOME to the second generation of content-editing in Drupal!

8)

Roi Danton’s picture

Thanks for providing the patch for img_assist, too! In case someone wonders the 6.x-2.x-dev release can be found here.

As far as I understand it is enough to use hook_wysiwyg_plugin() to add buttons to the supported editors with my module (as img_assist does). The things the plugin array has to contain seems to be quite self-explanatory. The possibility to use own dialogs is really great! Thx (and I shut up now and will write follow ups in separate issues as you requested ;) )!

sun’s picture

Thanks for reminding and pointing that out. 2.x is now displayed on the project page.

quicksketch’s picture

Exciting. Thanks sun!

aread22’s picture

Is the teaser break plugin really working? I have the latest 6.x-2.x-dev release and TinyMCE 3.2.1.1 and have enabled the "Teaser Break" plugin in the Buttons area, but I do not see a button in the textarea. Thanks

tstoeckler’s picture

@aread22:
sun:

All issues related to this patch as well as improvements related to @todo statements in this patch should be treated in separate issues. Thanks in advance.

Please DO open up a new issue for your problem, I am certain sun is desperately waiting for this issue to be finally closed.

Owen Barton’s picture

Version: 6.x-2.x-dev » 5.x-1.x-dev
Status: Fixed » Needs review
FileSize
31.3 KB

Here is a patch that backports the rest of these changes to the 5.x version (some were already committed). The break seems to work well, but I haven't tried the dialog code yet.

sun’s picture

Status: Needs review » Fixed

Thank you! :)

Committed, erm, that patch + a bit more trickery to DRUPAL-5--2.

Marking as fixed. Please re-open if anything of this was just "mad nuts"... (possible, since untested).

sun’s picture

Committed a follow-up fix for the D5-theme-trickery.

Owen Barton’s picture

Version: 5.x-1.x-dev » 5.x-2.x-dev
Category: task » bug
Status: Fixed » Needs review
FileSize
802 bytes

One more fix to the 2.x dialog code - there is no theme_render_template function in Drupal 5 (this functionality was only moved from the phptemplate engine in Drupal 6).

sun’s picture

Status: Needs review » Fixed

Sorry, this fell somehow out of my radar. Committed!

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.