- tech ...
A few weeks ago, I spent a long long time building a feature in Drupal that allows users to drag and drop files from a "media bin" block into a TinyMCE rich-text editor. Users click on a thumbnail of an image, for example, drag and drop into the editor, and the full-sized image appears. In order for this to work, a bunch of disparate software components (TinyMCE, Drupal, jQuery, Quicktime), each with their own adorable quirks, had to talk to eachother. It was rather like assembling a working watch from the parts of 5 different watches. The bulk of my time was spent dealing with a parade of poorly-documented problems, which I describe below in the hopes that they may prove useful to someone who has to familiarize themselves with the details of these software packages on a tight deadline. I'm too lazy to tidy up the code for presentation, but there're plenty of links. Making thumbnails draggable is trivially easy with the jQuery Interface library. I put the URL of the full-sized image (or video or mp3 or whatever) into the "title" attribute of the thumbnail, so that the browser has access to it without making a request to the server.
PROBLEM: There is no easy way to tell the TinyMCE window to accept dropped thumbnails. CAUSE: The TinyMCE editor doesn't exist when document.ready() is called, so there is no way to turn the editor into a Droppable() when the document finishes loading. SOLUTION: I wrote a TinyMCE plugin that makes the editor Droppable in the initInstance() hook. That way, the editor makes itself into a Droppable when it's good and ready.
The onDrop code is pretty simple as well. You just have to construct a snippet of HTML based on the thing that's being dropped, and insert that snippet into the editor using tinyMCE.execInstanceCommand(). If the thumbnail that's dropped is of an image (as determined by its class), build an IMG
tag based on its attributes. If it's audio or video, build an EMBED
tag (except not. More on this later.).
PROBLEM: TinyMCE appears to modify URLs when it refreshes the canvas. If you're on the page "/foo/bar/index.html", and you create a link to something like "/image.jpg" in TinyMCE, you'll find that when you move that image around, it disappears because its URL gets changed to "../../image.jpg". This should resolve correctly, but on my Drupal system, it didn't. CAUSE: TinyMCE marshals many lines of code to accomplish this munging, because it's actually a counter-measure for the URL rewrites that are apparently part of the Gecko editable-canvas window. I guess Gecko does the rewriting, and TinyMCE tries to counteract that with its counter-munging. These new URLs didn't work with the Drupal installation on our server, apparently because all our development sites are in subdirectories on our development server. The base URL included these subdirectories, but the munging wasn't taking them into account somehow. SOLUTION: We set up a subdomain for this site, so that it could reside at top-level while we were working on it. I think the Drupal TinyMCE plugin needs to work on this.
PROBLEM: Once I got images working, it seemed like getting audio and video working would be trivial, and I gleefully reported this to my co-workers. Unfortunately, getting any HTML more complicated than images (such as a Quicktime EMBED
tag) to display in TinyMCE proved difficult. The HTML that I dragged into the editor just wouldn't show up, even when I dragged a movie in and disabled the rich-text editor.
CAUSE: At first, I tried to examine this by dragging the code into a plain textarea and then enabling rich text. Nothing happened, and nothing showed up in plaintext when I dragged a movie into TinyMCE and then clicked "disable rich-text". Turns out, TinyMCE keeps a list of tags that you're allowed to use, and filters out exotic ones like OBJECT
with extreme prejudice unless told otherwise.
SOLUTION: To tell it otherwise, I enabled the media plugin. Plugins can modify the list of allowed tags, and the media plugin opens the door for Quicktime embeds and other fun things.
PROBLEM: Enabling the plugin still didn't get me my QuickTime icon when I dragged and dropped.
CAUSE: Aside from filtering, TinyMCE performs other operations on HTML that make it such that the code I saw in the textarea did not accurately reflect the code that TinyMCE was rendering. Specifically, the QuickTime icon I was hoping to see was not some quirky rendering of a Quicktime plugin OBJECT
, but was actually just a div with an image. The media plugin converts this DIV to OBJECT code when the form is submitted or the rich editor is disabled.
SOLUTION: Instead of attaching OBJECT
code to QuickTime draggables, I attached DIV
code that matched the code that TinyMCE generates from OBJECT
tags. Worked like a charm: it gets displayed properly and is automatically converted to the right OBJECT
html. At this point, the basic functionality was complete.
PROBLEM: Usually when you drag and drop an image into text, the text caret moves around with the mouse pointer, so that the image shows up where you dropped it, not where the text caret was when the user left the editing box. This wasn't happening in TinyMCE. It happens when you drag things from one spot in the editor to another, but there doesn't seem to be a way to get that behavior when you're dragging something in from outside the box. CAUSE: TinyMCE doesn't have a good way of moving the caret around based on mouse position, because browsers don't have good semantics for this. SOLUTION: None. Wait for this kind of thing to become supported. I need to file a bug report.
PROBLEM: In FireFox, images don't do a good job of keeping up with the mouse cursor when they're dragged. After about half a second, the image catches up, unless the mouse is over the TinyMCE window, in which case the image hangs out at the edge of the window. The only way to get the image to catch up with the pointer is to move the pointer back outside the TinyMCE window. Unfortunately, the TinyMCE window is exactly where users will want to drop images, so this bug is actually rather serious.
CAUSE: The TinyMCE editor operates within an IFRAME
, and IFRAMEs have their own separate DOM. When the mouse goes over the TinyMCE box, the mouseOver event goes to the IFRAME DOM, not the DOM of the larger document. The image that we're dragging has no way of getting that mouseover event, because it's part of the outside DOM and its mouse events are being intercepted. It's Supposed To Be That Way, according to Mozilla.
Crucially, when the pointer is over the image and the image is over TinyMCE, it's still in the parent window DOM, because the image is in the parent window DOM.
SOLUTION: If FireFox improved its dragging responsiveness, this would not be an issue. If FireFox changed its semantics to fire mouseover events in the parent window when drags were going on, that would solve the problem. I actually tried surrounding each of the draggable images a "trampoline" DIV that extends for 200px in each direction from the draggable image, so that there was always something between the mouse and the editor, but that was complicated (if you want to try that in certain Draggable modes, you have to access the copy of the image, not the image itself).
In the end, I realized that TinyMCE has an addEvent() function for attaching hooks to the IFRAME. I attached hooks that passed the events to a callback in the parent window. The callback called the onDrag() function of the Draggable, which, according to the Interface docs, allows me to alter the resultant coordinates of the image.
PROBLEM: That's bullshit. The onDrag() function does no such thing. CAUSE: There's a separate callback, onDragModifier(), that does what we want here, but the most recent docs don't mention it. In fact, the most recent docs don't mention a host of useful parameters that callbacks like onDrag() can take. SOLUTION: Submit new documentation, and use onDragModifier(). You basically adjust the position of the image to correspond with the coordinates you get from the IFRAME mousemove event.
PROBLEM: When you tell the image to assume the coordinates of the cursor over an IFRAME, the image winds up in either the upper-left or bottom-right corner of the screen. CAUSE: The coordinates that get passed from the IFRAME are relative to the IFRAME, not the larger window. Also, the coordinate system in native DOM events is different than the one jQuery uses in its events (which seem to represent the location of an item relative to its original position, rather than the absolute screen position). SOLUTION: Do some math to adjust the coordinates. I had to take the starting point of the image into account, and add the position of the TinyMCE window, or something, but in the end it worked really well. Make sure that code only gets applied when the browser is FireFox.
PROBLEM: Nothing works in Safari. CAUSE: the mceInsertContent command of TinyMCE is totally broken for Safari. In fact, its whole browser-checking mechanism needs a tune up. SOLUTION: Submitted a bug, working on a patch.
See, that wasn't so hard now, was it?