Using Greasemonkey to add a toolbar button to the Confluence rich text editor?

Graham Hannington July 19, 2012

If you have any experience doing this, I'd be very interested in your code as a head-start. I'm interested in this option instead of a plugin because I want to develop extensions to the editor that are available to all Confluence users, including OnDemand users (er, okay: all users with Greasemonkey or similar add-on/extension that enables user scripts).

Specifically, for my first attempt, I'm considering adding a toolbar button that:

  1. Selects the contents of the rich text editor iframe body (the "RTE HTML"; the editable page contents).
  2. Uses the GM_xmlhttpRequest (which allows cross-domain requests) to get the Wikifier RT XSLT stylesheet (that converts RTE HTML to wiki markup).
  3. Converts the RTE HTML into wiki markup (using built-in in-browser XSLT processing, as done by Wikifier RT).
  4. Opens the Insert Wiki Markup dialog showing the converted wiki markup. (I'd appreciate tips on this: specifically, JavaScript to open that dialog with content supplied by me.) Clicking the Insert button on the Insert Wiki Markup dialog should (because the page contents are, hopefully, still selected) replace the entire page contents.

What I'm trying to do here is integrate Wikifier RT functionality with the rich text editor in a way that does not exclude OnDemand users.

(I have other ideas to extend the editor, too, such as XSLT-based and regex-based find'n'replace functionality.)

Any thoughts, feedback, working JavaScript code?

6 answers

1 accepted

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

2 votes
Answer accepted
Petch
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 30, 2012

Graham.

You can get access to the editor via AJS.Rte.getEditor()

Graham Hannington July 30, 2012

Hi Craig,

Thanks very much!

The following code in my Greasemonkey user script opens the Wiki Markup dialog:

location.href = 'javascript:void(AJS.Rte.getEditor().execCommand("InsertWikiMarkup", true));';

Next: I want to set the value of the textarea element (id="insertwikitextarea") of the Wiki Markup dialog.

I would like to pass a string to the InsertWikiMarkup command (as the third parameter of the execCommand call), but that doesn't seem to work; possibly because I need to pass the string as, say, an item inside an array, rather than a simple string value.

Can you pass a value to the InsertWikiMarkup command to populate the textarea? If so, how?

Otherwise, I'll have to set the textarea value in that "Wiki Markup" child window after calling the execCommand.

I have tried various things without success, including the following:

alert("An arbitrary delay.");
  location.href = 'javascript:void(window.open("", "confluence.conf_wikimarkup", "").document.getElementById("insertwikitextarea").value = "some content");';

That "arbitrary delay" alert is a placeholder until I figure out some better (handler-based?) method for waiting to set the textarea value until the child window has loaded. If the InsertWikiMarkup command doesn't support being passed wiki markup to display in its textarea, then I'd appreciate any tips you could offer me in this area.

In a recent comment, I see yet another customer being lost due to the removal of a wiki markup editor view:

Our whole department have rebelled ... No wiki markup? No Confluence.

I understand Atlassian's oft-stated position ("we're sorry to lose you, goodbye"). I wonder why I care. It's not like Wikifier offers 100% bulletproof conversion, but - for what it's worth - I'm inching closer to providing single-click Wikifier functionality inside the editor (rather than the faff of copying'n'pasting to'n'from an external web page), and I'd appreciate whatever technical assistance you can offer me.

Graham Hannington July 30, 2012

I'm inching closer to providing single-click Wikifier functionality inside the editor

I am surprised that someone with more time than just a few spare minutes after work (the reason for my snail-like progress) has not already done this. I continue to hope that someone will. Then I can relax, forget about it, and move on.

Graham Hannington July 30, 2012

Hi again Craig,

While I'm asking for coding tips... my first thought for this new Wikify toolbar button was to use the XSLT stylesheet from Wikifier RT that converts RTE HTML. However, if possible, I think I would rather convert the Confluence storage format XML (using the XSLT stylesheet from the original Wikifier). Converting RTE HTML is, after all, a completely bonkers thing to even consider, let alone attempt. It occurs to me that I could simply call a Confluence JavaScript function to convert the current rich text editor contents into Confluence storage format XML. Could you please provide me with a snippet of JavaScript that demonstrates calling this function (or otherwise just point me to the relevant Confluence JavaScript API reference documentation)?

Petch
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 31, 2012

Looking at the source (if you have the source distribution of Confluence you can find it in confluence-plugins/confluence-misc-plugins/confluence-tinymce-plugin/src/main/resources/tinymce3/plugins/insertwiki/editor_plugin_src.js), it's not possible to pass in the text for the dialog.

A better approach may be to access the rest resource that does the wikimarkup -> editor format conversion, and handle success / failure yourself. (Basically do a variation of what the insertwiki/editor_plugin_src.js is doing).

If you want to continue down the current path, there should be an event triggered when the dialog is opened (it should be a standard AUI event). Something like:

AJS.bind('show.dialog', function(e, dialog) { 
  AJS.log(e); 
  AJS.log(dialog); 
});

I don't think there are any REST services for converting from wiki-markup to storage format. There are rest services for editor<->storage format conversion if the Source Editor is installed.

Other tips:

Not sure why you're doing this:

location.href = 'javascript:void(window.open("", "confluence.conf_wikimarkup", "").document.getElementById("insertwikitextarea").value = "some content");';

Just do this if you just want a "delay" something, just use something like setTimeout:

setTimeout(function() {
   // do something
}, 1000);

Graham Hannington July 31, 2012

Hi Craig,

Thanks very much for the reply.

Regarding that admittedly very odd-looking location.href assignment statement: that's a "Greasemonkey thing" known as a location hack (follow that link only if you want to do your head in ;-).

To me, time-based delays (waiting in the hope that enough time has passed for some event to have occurred; perhaps using a loop that performs some check after a relatively short delay) are inherently inelegant, and to be avoided if possible. I know how to use setTimeout() - thanks; you weren't to know whether I did or not - but I would prefer a cleaner solution, such as setting an event handler. Thanks very much for the AUI tip. My JavaScript skills fall somewhere between complete idiot and newbie (although, hey, I did manage to code the Wikifier web page, which does client-side XSLT in all major browsers).

Regarding "A better approach...": at this stage, I like the idea of copying wiki markup into the Wiki Markup dialog, because I think that saves me writing code: I get the "editor" (textarea), conversion to RTE HTML and replacement of the current editor selection (the Insert button) for "free".

Regarding this:

There are rest services for editor<->storage format conversion if the Source Editor is installed.

Ah. If I interpret that correctly, I think it means that, if the Source Editor plugin is not installed, then there is no function/method provided by Confluence, and available in client-side JavaScript, that will convert the current editor contents (RTE HTML) into Confluence storage format XML. Is that true? If so, then this clarifies my choice of XSLT stylesheet: if I want the "Wikify" toolbar button to work for OnDemand users (or anyone else who does not have the Source Editor plugin), then I'll need to convert the RTE HTML.

Petch
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 31, 2012

Graham.

Interesting note about the location hack to get around some Greasemonkey sandboxing. I agree using setTimeout is generally a bad idea (I wasn't passing judgement ;) ).

Re: conversion rest services - that's correct you don't get this without the source editor (which isn't in OnDemand). You'll need to convert to the Editor format if you want to generate HTML yourself.

Graham Hannington August 2, 2012

Hi Craig,

To recap: I can use the TinyMCE execCommand method to display the Wiki Markup dialog. Now that I've displayed it, could you please show me how to get a window object for the dialog? I'm beating my head against this but making no progress. When I have the window object, I can set the value attribute of its descendant (via the document object) textarea element. When I have that "proof of concept" working, I'll insert the Wikifier JavaScript into the Greasemonkey script. Not much point doing that right now (given the approach I want to take) until I can insert content into the Wiki Markup dialog's textarea. I'd really appreciate some help with this: my Google mojo (Stackoverflow, TinyMCE sites) has failed me.

Petch
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 2, 2012

Hi Graham,

The following function works for me (well in 4.3 rc - I wouldn't expect it to be different for earlier 4.x releases).

function insertWikiMarkup(markup) {
	AJS.Rte.getEditor().execCommand('InsertWikiMarkup');
	AJS.$('#insertwikitextarea').val(markup);
	AJS.$('#insert-wiki-markup-dialog button.button-panel-button').click();
}

I didn't need to listen for dialog events, which is nice.

Integrate something like that in your greasemonkey location hack and you should be fine.

Graham Hannington August 3, 2012

Hi Craig,

You are very kind, thank you so much for your help.

I now have a "Wiki markup" toolbar button in the Confluence rich text editor that selects all content, and then displays its wiki markup in the Wiki Markup dialog.

Here is the function that gets called when I click "Wiki markup":

function customAction()
{
  var iframe = document.getElementById("wysiwygTextarea_ifr");
  var RTEHTML = iframe.contentDocument.body.innerHTML;
  var wikiMarkup = wikify(RTEHTML);
  // Get the Atlassian JavaScript abstraction object
  var AJS = unsafeWindow.AJS;
  // Get the RTE object
  var ed = AJS.Rte.getEditor();
  // Select entire contents
  ed.selection.select(ed.getBody(), true);
  // Display the Wiki Markup dialog
  ed.execCommand("InsertWikiMarkup");
  // Insert the wiki markup into the dialog
  AJS.$("#insertwikitextarea").val(wikiMarkup);
}

The wikify() function uses the same XSLT stylesheet as the Wikifier RT web page. The script uses the Greasemonkey function GM_xmlhttpRequest() to get the .xsl file from the Wikifier RT website. This function allows requests to cross the "same origin policy" boundaries.

I am the first to admit that this XSLT stylesheet is far from comprehensive. I can (and probably should) list combinations of markup that it currently does not correctly convert. It covers the limited formatting that I use (well, most of it); right now, I'm deliberately not spending time trying to make it any more comprehensive without specific requests. I'm open to enhancing it as users meet its limits. I realize that there's a chicken-and-egg issue here: users might not want to even begin using it until it's comprehensive; they might not want to act as testers.

Still, in a small voice, and being fully aware of its current restrictions (some of which are, effectively, insurmountable), and the requirement for Firefox and Greasemonkey (although I believe there are similar add-ons for some other browsers): welcome back, wiki markup editor.

Graham Hannington August 5, 2012

Hi Craig,

I've just taken a look at your wiki-styler.js on Bitbucket, with the idea of replacing my "direct DOM insertion" method of adding the toolbar button with some TinyMCE API calls such as ControlManager.createButton. Turns out you're doing effectively the same thing as me (albeit via jQuery), so I'm going to leave that part of my code as is for now.

Graham Hannington August 5, 2012

Hi Craig,

I think I might have to listen for the Wiki Markup dialog to load, after all. I think execCommand() is asynchronous; or perhaps, more specifically, the function that implements the InsertWikiMarkup command is asynchronous. Occasionally, I get a blank Wiki Markup dialog, even though the wiki markup conversion was successful (I write it to the console log). I can only put this down to the ".val()" statement running before the dialog has actually loaded. I'm going to try using that "bind" code you suggested. Please feel free to spoonfeed me working code (a "bind" for the Insert Wiki dialog). I'm not proud. I've just spent a largely unproductive evening attempting to add some error-checking code to the GM_xmlhttpRequest call. Debugging Greasemonkey scripts is turning out to be tedious. I'm keen to publish this user script, but I'm not going to do that while I can get it to fail. Frustrating!

Graham Hannington August 5, 2012

Well, this is strange... perhaps the problem has nothing to do with asynchronicity, after all. I can reliably reproduce the problem (the blank Wiki Markup dialog, even when I know the wiki markup conversion has worked, because I've written it to the console) by pressing the Esc key to close the Wiki Markup dialog. If I click the Cancel button with the mouse, it seems to keep working forever, but after pressing the Esc key instead, every time I click the "Wiki markup" toolbar button, I get the successfully converted wiki markup logged to the console, but the Wiki Markup dialog is empty.

Can you reproduce this? What is special about pressing the Esc key (as opposed to clicking the Cancel button with a mouse) that might cause this to happen? I hope it's not a Greasemonkey thing.

Last thought for the evening (probably just fuzzy thinking): I'm wondering whether pressing Esc does something to make AJS.$("#insertwikitextarea") not "find" anything.

Graham Hannington August 6, 2012

To avoid exceeding the "characters left" limit on comments in Atlassian Answers, I have copied the complete draft source of the Greasemonkey user script to a new page, "Wiki markup toolbar button", on the Confluence Advanced Tips wiki, so that you can see it and try it for yourself, if you have the time and inclination (I understand that you might reasonably have neither, no problem).

That page is a temporary home for the draft source: when I think the user script it's ready for general use, I'll upload the source to userscripts.org.

Graham Hannington August 6, 2012

This is really doing my head in. I have done a bunch of Googling and reading about textarea elements, and the value property (versus attribute), and jQuery attr(), prop(), and val(), and why not to use innerHTML, and using the direct (non-jQuery) .getElementById().value =, but none of the techniques that I have tried work after I press the Esc key to close the Wiki Markup dialog. It's maddening.

Before using one of the above techniques to set the textarea value, I have inserted the following code to test that the textarea exists:

if (AJS.$("#insertwikitextarea").length &gt; 0)
  {
    alert("Dialog exists. Length: " + AJS.$("#insertwikitextarea").length);
  }
  else
  {
    alert("Dialog does not exist");
  }

It always exists.

What the heck is it about pressing the Esc key that causes this problem?

I'm wondering whether TinyMCE is involved. Is the textarea in the Wiki Markup dialog a TinyMCE editor? Rather than setting the textarea value directly (either via direct DOM methods or jQuery), perhaps I should be calling the setContent method? The TinyMCE FAQ indicates that this might be so, but I have no idea what to use for the editor name and/or format parameter. If this is the root of the problem, could you please show me how to access the TinyMCE editor object that corresponds to the textarea element of the Wiki Markup dialog (and, if necessary, specify the correct format parameter for setContent), so that I can use setContent on that editor?

The Wiki markup button works... until you press the Esc key to close the Wiki Markup dialog! Grrr :-(. Any advice you could offer me would be greatly appreciated.

Petch
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 7, 2012

Graham,

Sorry for the slow reply I've been away from Answers for a while.

It seems there's a bug, when pressing escape, the dialog is just closed, and not removed from the DOM. On opening the dialog again, instead of reusing the existing dialog a new one is created.

So there are 2 dialogs with textarea with id = insertwikitextarea. This is obviously a bad thing and JQuery on returns the first textarea it finds.

The simplest workaround for you is you use the following instead:

AJS.$("textarea[name='wikitext']").last().val(wikiMarkup);


Graham Hannington August 7, 2012

Crikey, no need to apologize: here I am, an AJS/TinyMCE/jQuery newbie bleating to be spoonfed code - to achieve (make a half-assed attempt at) something that Atlassian has clearly stated it does not want to do (bring back the wiki markup editor) - and you're taking the time to help me. I have nothing but respect and gratitude.

Yeah, I suspected that the problem might have been a previous instance of that dialog (and its textarea) lying around, which is why I inserted that "length" check (and echoed its value in an alert). But I now realize (although I haven't done any further investigation on this) that my use of an ID-based selector for that check was ill-considered, because length would only ever have returned 1 (because IDs are, as you point out, supposed to be unique).

Anyway, the workaround that you have given me works. Thank you very much!

Graham Hannington August 7, 2012

Er, actually, that workaround only half-worked. Or rather, the workaround works perfectly, but I now have another problem, which also appears to be triggered by pressing the Esc key, and so might also be caused by the same bug that you mentioned: after pressing the Esc key to close the Wiki Markup dialog, any edits that I perform in any subsequently opened Wiki Markup dialog are not honored when I click the Insert button on the dialog.

I can reproduce this bad behavior in the Confluence rich text editor without any of my Greasemonkey shenanigans. So, it could be experienced by any user.

Can you reproduce this behavior? If so, could you please open a JIRA issue for this?

Joe Clark
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 12, 2012

I need to look in the database to double-check, but I think this is the most-commented answer on the site now :-)

Edit: Ah, sadly not even close:

answer comments
53724 24
63513 23
68931 22
49351 22
62137 21
44940 21
12560 20
40256 20
39572 20
52364 20
23047 19
51070 19
60272 18
46797 18
51813 18
13589 18
29629 18
43185 17
29157 17
64628 17
20492 17
44629 17
73295 17

Petch
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 12, 2012

Graham,

I've raised this issue: CONF-26299

Feel free to add any further details / corrections.

1 vote
Joe Clark
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 26, 2012

I was about to suggest that you try out writing a Speakeasy Extension, but I don't think that is an enabled feature in OnDemand either :-(

I think Greasemonkey is dark magic and wish I knew more about it, so can't help you there :-)

However, I can tell you that we have an upcoming Confluence plugin developer tutorial blitz coming up next month - the 2012 Confluence Doc Sprint. One of the tutorials we're hoping to build is 'Adding a new icon to the editor toolbar' - so if you're willing to wait a little bit, we should hopefully have some better documentation on this in the near future.

Graham Hannington July 26, 2012

Hi Joseph,

Thank you, I really appreciate you taking an interest in this question.

I looked at Speakeasy a while back (it seemed like a good option, as I am currently only looking at writing client-side - JavaScript - code), but, as you say, I think that is not currently available in OnDemand.

I'm with you on the "dark magic", but if I want to extend the editor now in a way that does not exclude OnDemand users, I can think of no other option than to cavort at night with a slippery simian under the flickering light of a firebug ;). If you can suggest another method, I would welcome it.

Having said that, I will definitely be interested in reading that tutorial; thanks for the heads-up.

Graham Hannington July 29, 2012

Hi again Joseph,

I've developed a rudimentary, proof-of-concept user script that adds a custom button to the Confluence rich text editor toolbar. The button doesn't do much - it just displays an alert box showing the HTML of the current selection (if any) - but it demonstrates the basic concept.

I have not tested it extensively, but it seems to work okay so far.

Graham Hannington July 29, 2012

Hi Joseph,

Would you please take pity on me and provide me with a snippet of JavaScript that shows me how to display the Insert Wiki dialog (containing wiki markup that I provide) in the Confluence rich text editor? I believe this means calling the execCommand method of the editor object with "InsertWikiMarkup" command as the first parameter, but I'm struggling to know how to refer to the editor object (the stuff before the period in .execCommand("InsertWikiMarkup" ... ).

0 votes
Graham Hannington August 8, 2012

You can download the Greasemonkey script discussed here from the "Wiki markup toolbar button" page on the Confluence Advanced Tips wiki.

0 votes
Joe Clark
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 26, 2012

I took the liberty of adding the 'confluence-ondemand' tag to this question, I hope you don't mind :-)

0 votes
Joe Clark
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 26, 2012

The wizard that pops up when you first ask a question just controls which tags get applied to the question. You could manually add both the 'confluence' and 'confluence-ondemand' tags to the question to achieve what you desire.... however, I assume that the pretty banner at the top of the question will choose one or the other and doesn't have a way to display both.

0 votes
Graham Hannington July 19, 2012

Is there a way to create a question that applies to the "installed versions" and "OnDemand"? This question applies to both. When I asked this question, I deliberately did not select either "installed" or "OnDemand". However, I notice that the question appears to have defaulted to "applies to the installed versions".

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events