XSLTForms/TinyMCE
Like other XForms implementations, XSLTForms supports a non-standard control for editing mixed content (XML with a mixture of elements and character data).
XSLTForms can (at least in principle) be used either with CKEditor or with TinyMCE. This page describes the use of TinyMCE with XSLTForms. It is based on version 638 of XSLTForms; behavior may vary in other versions.
Framework for using TinyMCE with XSLTForms
editTo use TinyMCE as a control in an XForm, several things are necessary. A simple minimal example of using TinyMCE is available on the AgenceXML web site; it illustrates all the following points.
- A
script
element should point to the TinyMCE library and specify the TinyMCE version number. - An XSD schema should be provided, which defines an appropriate simple type for the element to be edited using TinyMCE; an
xsd:appinfo
element in the definition of this type contains the initialization object used by TinyMCE to customize the editor. - The element to be edited using TinyMCE should be bound to the simple type declared in the XSD schema.
Pointing to the TinyMCE Javascript and supplying a TinyMCE version number
editInclude an XHTML script
element with the following attributes:
type
with the valuetext/javascript
src
provides a reference to the TinyMCE library (relative to the XForm)data-uri
specifies which rich-text editor is being used; for TinyMCE, use the valuehttp://www.tinymce.com
data-version
specifies what version of TinyMCE is being used
The content can be vacuous; a Javascript comment (/* */
) will do.
For example:
<script type="text/javascript"
src="xsltforms/scripts/tinymce_4.0.21/tinymce.min.js"
data-uri="http://www.tinymce.com"
data-version="4.0.21"
>/* */</script>
The data-version attribute is used by XSLTForms to adjust its calls to TinyMCE; it distinguishes versions 3 and 4 of TinyMCE. (Or, to be more precise, it distinguishes versions whose number begins with "3." and all other versions.) Note, however, that not all versions of TinyMCE 4 work with XSLTForms version 638 (see below).
Supplying a richtext type and a TinyMCE initialization object
editA key part of using TinyMCE with XSLTForms is to provide an xsd:schema element, which can be embedded in the XHTML header. A very simple example follows:
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xforms="http://www.w3.org/2002/xforms"
targetNamespace="http://www.agencexml.com/xsltforms/rte">
<xsd:simpleType name="standardHTML">
<xsd:restriction base="xforms:HTMLFragment" xsltforms:rte="TinyMCE"/>
<xsd:annotation>
<xsd:appinfo> {
plugins: [ "code" ],
toolbar: 'undo redo | bold italic | bullist'
} </xsd:appinfo>
</xsd:annotation>
</xsd:simpleType>
</xsd:schema>
As can be seen, this schema defines a simple type with a number of properties worth noting:
- The type's expanded name is
{http://www.agencexml.com/xsltforms/rte}standardHTML
. (It's not clear whether the name must be this one or can vary. [Conjecture (unconfirmed): it can vary, so that different instances of TinyMCE with different customizations can co-exist in the same form.]) - Its base type is
{http://www.w3.org/2002/xforms}HTMLFragment
. (This appears to be essential; there is code in xsltforms.js which checks for this.) - The
xsd:restriction
element in its definition carries the attribute-value specificationxsltforms:rte="TinyMCE"
(where the prefixxsltforms
is bound to namespacehttp://www.agencexml.com/xsltforms
). XSLTForms uses the value of this attribute to distinguish code specific to TinyMCE from code specific to CKEditor. - The type definition contains an
xsd:appinfo
element whose character-data content is a Javascript object. Strictly speaking, theappinfo
element is optional (a default of{ }
is assumed if none is present), but in practice it will almost always be needed. The Javascript object described in theappinfo
element will (with some modifications) be passed to TinyMCE as the argument to the TinyMCEinit()
function; it is the essential mechanism for customizing the TinyMCE.
Binding the instance to the richtext type
editThe element(s) to be edited using TinyMCE should be bound to the type declared for them in the schema described above.
For example:
<xf:bind nodeset="richtext" type="rte:standardHTML"/>
Note that the content of the element to be edited should be character data only: a string, with no child elements. The string may (and normally will) contain the serialized XML representation of elements and attributes.
See Serial form and XML form, below.
Serial form and XML form
editThe use of mixed-content editors like TinyMCE and CKEditor requires that the form designer distinguish carefully between what might be called the normal XML form of an XML document or fragment and the serialized form of the same material. Some other constructs pose the same challenge, like the transform()
and serialize()
functions and the setnode
action. (This topic has broad potential for confusion, since it is the serialized form of an XML document which is defined by the XML specification; the fact that we will sometimes be writing the serialized form of an XML element within an XML document also complicates things. The reader who finds the discussion here confusing is asked to be patient; experimentation with a form may also help.)
XML form
editWhat we here call the XML form of an instance document is represented in two different ways at different moments:
- In the XHTML+XForms document itself, it's represented as a normal XML document or element, using XML syntax.
- in a running instance of a form being delivered with XSLTForms, it's represented as a DOM object.
The former is illustrated by the following XForm fragment, showing an instance named normal-document whose root element is named data
and has one child named richtext
, which in turn has a child named charge
.
<xf:instance id="normal-document" xmlns="">
<data><richtext>
<charge>
<p>uncertain<en>
<p>Gruen (<i>Historia</i> 1966) 38 suggests a
<procedure pid="c-maiestas" lang="lat">maiestas</procedure>
trial for seditious behavior as tribune.</p>
</en></p>
</charge>
</richtext></data>
</xf:instance>
When this XML is read by an XForms processor, a data structure is built to represent it, following the rules of the Document Object Model (DOM).
In editing mixed content, however, TinyMCE does not operate on the DOM representation of the data; it operates on a string containing tags marked with angle brackets: the serialized form of the data to be edited. (The reasons for this design choice are obscure.)
Serialized form
editIf a document instance is intended to be edited using TinyMCE, using the normal XML form for the document will lead to unsatisfactory results. (TinyMCE will get the string value of the richtext
element, perhaps wrap it in a p
element (depending on configuration), and lose details like the attributes on procedure
.) To be usefully editable using TinyMCE, the instance document must be presented in serialized form, that is as a string containing a sequence of characters including angle brackets. To write such a string in an XML context (e.g. an XForm), one must escape the left angle brackets and ampersands appearing in the serialized form of the data. When this is done, the xf:instance
element will look something like this:
<xf:instance id="serialized-document" xmlns="">
<data><richtext>
<charge>
<p>uncertain<en><p>Gruen (<i>Historia</i> 1966) 38 suggests a <procedure pid="c-maiestas" lang="lat">maiestas</procedure> trial for seditious behavior as tribune.</p></en></p>
</charge>
</richtext></data>
</xf:instance>
Or alternatively (using a CDATA marked section) like this:
<xf:instance id="serialized-document-2" xmlns="">
<data><richtext><![CDATA[
<charge>
<p>uncertain<en>
<p>Gruen (<i>Historia</i> 1966) 38 suggests a
<procedure pid="c-maiestas" lang="lat">maiestas</procedure>
trial for seditious behavior as tribune.</p>
</en></p>
</charge>
]]></richtext></data>
</xf:instance>
Like the XML form, the serialized form of an instance document is represented in two different ways at different moments:
- in a running instance of a form being delivered with XSLTForms, it's represented as a string of characters which conforms to XML syntax rules.
- In the XHTML+XForms document itself, it's represented as an escaped string.
Moving back and forth
editIf TinyMCE is to be used on a document instance (or part of an instance) which has a normal XML structure, then it will be necessary to serialize the document. The serialize()
function is helpful here. After the user has edited the serialized version of the data, if it is desired to restore its normal XML structure, the serialized data must be reparsed; the xf:setnode
extension element is useful for this purpose.
[Example needed, showing movement back and forth from normal form to serialized form.]
Styling the control
editThe XForms rich-text control can be styled in the usual way for XSLTForms.
For example, the following style
element sets the font family, height, and width for textarea controls within an element of class large-textarea
.
<style type="text/css">
.large-textarea textarea {
font-family: Courier, sans-serif;
height: 10em;
width: 500px;
}
</style>
The actual XForms control can then be linked to this style simply by assigning it to the class large-textarea
:
<textarea ref="richtext"
class="large-textarea"
mediatype="application/xhtml+xml"/>
For more information on styling controls in XForms, see this wikibook's discussion of CSS.
Customizing the TinyMCE editor
editThe basic mechanism for customizing TinyMCE is the initialization object. This is a user-specified Javascript object which TinyMCE consults during initialization.
In the TinyMCE documentation, this is shown as the function argument in a call to the TinyMCE init()
function. Examples in the TinyMCE documentation may look something like the example below; this shows a simple initialization object with the properties selector
and toolbar
.
<script>
tinymce.init({
selector: '#mytextarea',
toolbar: 'undo redo | bold italic | bullist'
});
</script>
In XSLTForms, the call to the TinyMCE init()
function is handled by XSLTForms, not by the XForm author. So the initialization object is specified not in the call to init()
but as the character content of the xsd:appinfo
element in the simple type to which the element to be edited is bound. In the XSLTForms context, the selector
property is not used (if supplied, it is ignored, overwritten by XSLTForms), so the equivalent of the initialization object shown above will look something like this (the surrounding schema is not shown):
<xsd:appinfo> {
toolbar: 'undo redo | bold italic | bullist'
} </xsd:appinfo>
The following subsections describe a few simple customization examples, but for full information on the possible ways of customizing TinyMCE, see the documentation for the version of TinyMCE you are using. (The examples here use TinyMCE 4.)
With a few exceptions, every initialization property described by the TinyMCE documentation can be specified within the xsd:appinfo
element and XSLTForms will pass its value through to TinyMCE, so the effect will be as described by the TinyMCE documentation.
The exceptions are these:
- The
location
property (stressed heavily by the TinyMCE documentation, because required by TinyMCE) is supplied by XSLTForms, based on the type bindings of the data. Anylocation
value passed in by the user will be overwritten with an ID generated by XSLTForms for the particular control instance being initialized.
- The
mode
property (which doesn't seem to be described by the TinyMCE documentation, but may be used in aneditor.setMode()
call) is overwritten by XSLTForms with the value"none"
.
- The
setup
property (whose value is a function, which TinyMCE evaluates during initialization of an editor instance) is supplied by XSLTForms. Anysetup
value passed in by the user will be overwritten. TinyMCE customizations which require use of thesetup
property, including the specification of custom buttons, are thus not possible. For a workaround, see the discussion of custom buttons, below.
Styling elements within the TinyMCE editor
editA stylesheet to be used for the display of elements within the TinyMCE editor can be supplied by giving its URI (relative to the location of the XForm) as the value of the 'content_css' property in the TinyMCE initialization object. For example:
content_css: 'mce-control.css'
Setting the toolbar, menus, etc.
editThe toolbar
property can be used to specify what goes in the toolbar; it is helpful for eliminating unwanted tools in order to have a simpler interface.
'Custom' elements and 'valid' elements
editTinyMCE makes an effort to produce clean HTML, and to allow users of the TinyMCE library to constrain the HTML further.
If the initialization object specifies a list of valid_elements
, other elements in the data will be stripped out when the data are cleaned up (this happens before data are sent back to XSLTForms, as well as at other times). So valid_elements
can be used to restrict the data being edited to a specified set of elements.
Non-XHTML elements can also be specified, in the custom_elements
property.
XML elements not in XHTML
editIn a typical XForms application, the XML being edited is not necessarily XHTML; elements with names not known from the XHTML spec can be supported, by specifying their names as the value of the custom_elements
property.
[It appears to be necessary to specify custom elements both in the custom_elements
property and in the valid_elements
property. Testing for further information would be a good idea.]
It is also possible to specify what elements are allowed as children of a particular element. During data cleanup, TinyMCE may restructure the data if it thinks an element present in the data is not legal as a child of its parent. [Further details needed.]
Surprises, complications, problems
editVersioning issues
editWhere to find TinyMCE, where to put TinyMCE
editFor some time, XSLTForms has shipped with TinyMCE version 3.4.6; the code is placed within the XSLTForms directory at the location scripts/tinymce_3.4.6/tiny_mce.js
. When other versions of TinyMCE are used, it is convenient to download the minimized version from the TinyMCE web site and install it in an appropriately named subdirectory of scripts
. Other locations may also work.
TinyMCE has changed somewhat since version 3.4.6; some changes involve look and feel (TinyMCE 3 looks dated compared to 4), others involve the API.
Which versions of TinyMCE does XSLTForms support?
editXSLTForms 638 supports both version 3 and version 4 of TinyMCE; the minimal example of TinyMCE given on the AgenceXML web site uses TinyMCE 4.0.28. But not all versions of TinyMCE 4 appear to work with XSLTForms version 638; the most recent version that does appear to work is 4.3.12 (of May 2016). Later versions produce a "Loading..." message which never goes away, and an error message on the Javascript error console.
N.B. It is currently expected that release 640 of XSLTForms will support the most recent version of TinyMCE, 4.5.3.
Custom buttons
editThe addButton()
method
edit
To define custom buttons for the editor, the TinyMCE documentation suggests calling the addButton()
method on the TinyMCE editor object; the argument to the method is an object specifying a button identifier, a button label or icon, a function to run when the button is clicked, etc.
The TinyMCE documentation gives as an example:
editor.addButton('mybutton', {
text: "My Button",
onclick: function () {
alert("My Button clicked!");
}
});
The setup()
callback function
edit
An obvious first complication is that the editor object in question is not available until the TinyMCE init()
function is called. So the code shown above cannot simply be placed in a script
element in the page. Instead, the user supplies a callback function to TinyMCE, to be executed as part of initialization. (Some readers may know callback functions under other names like hooks or user exits.) The user does this by supplying the function as the value of the setup
property in the TinyMCE initialization object; the setup()
function gets one argument, the editor object.
<xsd:appinfo> {
// other properties here ...
setup: function(editor) {
editor.addButton('mybutton', {
text: "My Button",
onclick: function () { alert("Clicked!");
});
}
} </xsd:appinfo>
Button actions
editIn practice, other issues may arise. If the purpose of the button is to do for a custom phrase-level element what the builtin bold
and italic
buttons do for the HTML strong
and em
elements, then first the custom elements need to be declared and made valid, using the custom_elements
and valid_elements
properties described above.
Second, there must be functions to perform the action desired when the button is clicked. As is shown above, the function should take no arguments. In order to interact with the editor, therefore, we need to declare the function in a context where the TinyMCE editor object is available: namely, inside the setup()
function, where the editor object is passed in as an argument.
An example
editSuppose that we want to tag the currently selected text with a custom element, i.e. insert a start-tag before the current selection and an end-tag after it. To reduce redundancy in the code, we can define this with a generic Javascript function that takes the element type name as an argument, and any number of element-specific functions which call the generic function:
function insertElem(gi) {
var s = '<' + gi + '>'
+ editor.selection.getContent()
+ '</' + gi + '>';
editor.insertContent(s);
}
function insertPerson () { insertElem('person'); }
function insertProc () { insertElem('procedure'); }
Putting everything together into the initialization object, we get an appinfo
element like the following:
<xsd:appinfo> {
// other properties here ...
// adjust toolbar to taste, but don't forget to add the custom buttons
// 'insertperson' and 'insertprocedure'
toolbar: 'undo redo | insertperson insertprocedure italic | bullist',
custom_elements: "charge,~person,~procedure,en",
valid_elements: "charge,p,i,person[pid],procedure[pid],en",
setup: function(editor) {
function insertPerson () { insertElem('person'); }
function insertProc () { insertElem('procedure'); }
function insertElem(gi) {
var s = '<' + gi + '>'
+ editor.selection.getContent()
+ '</' + gi + '>';
editor.insertContent(s);
}
editor.addButton('insertperson', {
text: "Person",
onclick: insertPerson,
tooltip: "Insert person element"
});
editor.addButton('insertprocedure', {
text: "Procedure",
onclick: insertProc,
tooltip: "Insert procedure element"
});
}
} </xsd:appinfo>
Necessary modifications to XSLTForms 638
editThe remaining complication is that while the initialization object just shown will work with TinyMCE, by default XSLTForms overwrites the user-supplied setup
property with its own setup function.
To allow both the standard XSLTForms setup and the user-supplied setup to be executed by TinyMCE, three changes are needed in the code of xsltforms.js version 638 (applicability to other versions cannot, of course, be guaranteed). All three changes occur in the definition of a function named XsltForms_input.prototype.initInput
. In XSLTForms 638, this is easily found by searching for the string "setup
", which appears only in that function.
N.B. It is currently expected that release r640 will include the patch, or an equivalent patch, so the changes described here will be unnecessary.
1. After the line reading initinfo.mode = "none";
, insert the lines
initinfo.Xsltforms_usersetup =
(initinfo.setup
? initinfo.setup
: function (ed) {} );
This defines a property named Xsltforms_usersetup
(the name is chosen to minimize the likelihood of conflict with any new properties in future versions of TinyMCE), whose value is the user-supplied value of the setup
property, if there is one, and otherwise a vacuous function which does nothing.
2. Immediately following, there is a conditional testing for TinyMCE version 3.*; each branch of the if/then/else assigns a value to the setup
property.
In the then
branch, change the function by inserting a call to initinfo.Xsltforms_usersetup()
. Replace
initinfo.setup = function(ed) {
ed.onKeyUp.add(function(ed) {
...
});
ed.onChange.add(function(ed) {
...
});
ed.onUndo.add(function(ed) {
...
ed.onRedo.add(function(ed) {
...
});
};
with
initinfo.setup = function(ed) {
/* user exit for setup() */
initinfo.Xsltforms_usersetup(ed);
ed.onKeyUp.add(function(ed) {
...
});
// etc.
...
};
3. In the else
, do the same thing. For
initinfo.setup = function(ed) {
ed.on("KeyUp", function() {
...
});
ed.on("Change", function(ed) {
...
});
ed.on("Undo", function(ed) {
...
});
ed.on("Redo", function(ed) {
...
});
};
substitute
initinfo.setup = function(ed) {
/* user exit for setup() */
initinfo.Xsltforms_usersetup(ed);
ed.on("KeyUp", function() {
...
});
... etc. ...
};
With these changes, it becomes possible to define custom buttons for the XSLTForms mixed content control using TinyMCE.