CKEditor almost working, just need to rebuild model

93 views
Skip to first unread message

Fraser

unread,
Nov 16, 2009, 12:45:04 PM11/16/09
to Ubiquity XForms Developers
I've been trying to integrate the CKEditor and am stuck at rebuilding
the model. Using the following code I am able to replace the
xf:textarea with the CKEditor and pass the content of the editor to
appropriate element in the xforms instance upon each keyup, paste,
mousein and mouse out. Unfortunately the model.rebuild(), .recalculate
() and .refresh() methods don't seem to be working. The DOM
representation of the bound element is definitely getting updated, but
the output referencing it is not updating and any sent content is not
updated. Could anyone provide some insight into how to rebuild the
model?

/*
The following is xquery code to dynamically generate the textarea. I
pass a variable called $level which is like outline numbering and in
the form of 1.1.2.4. I use this as a unique idetnifier for this
instance of the editor but I have to tranlate it to remove the "."
You could also use the random() function or some other method to
create a unique identifier.
*/
let $translatedLevel := translate($level, '.', '')
return

<div>
<xf:textarea ref="{$ref}" incremental="true" id="{local-name
($data)}-{$translatedLevel}" class="editor">
<xf:label>{$level}. {content:getLabel(name($data),'',
$contentElementSchema)}</xf:label>
</xf:textarea>

<xf:output ref="{$ref}">
<xf:label>Instance Data</xf:label>
</xf:output>

<div style="clear:both">
<h2>Output</h2>
<div id="output-{local-name($data)}-{$translatedLevel}"></div>
</div>

<input type="button" onclick="alert(CKEDITOR.instances['{local-name
($data)}-{$translatedLevel}-editor'].getData())" value="Show Editor
Data" />

<script type="text/javascript">
var xfTextarea = document.getElementById( '{local-name($data)}-
{$translatedLevel}' );
var editorWrapper = document.createElement('div');
var editorWrapperStyle = editorWrapper.style.clear = 'both';
var editorDiv = document.createElement('div');
var editorDivID = editorDiv.setAttribute( 'id', '{local-name
($data)}-{$translatedLevel}-editor' );
editorWrapper.appendChild(editorDiv);
xfTextarea.appendChild(editorWrapper);
var editor = CKEDITOR.replace( '{local-name($data)}-
{$translatedLevel}-editor' );
var instance = document.getElementById
("contentObjectEntryEditInstance");
var content = instance.getElementsByTagName("atom:content")[0];
var element = content.getElementsByTagName( '{name($data)}' )
[0];
if(element.childNodes[0]){{editor.setData( element.childNodes
[0].nodeValue )}};
editor.on("instanceReady", function()
{{
this.document.on("keyup", update{concat(local-name($data),
$translatedLevel)});
this.document.on("paste", update{concat(local-name($data),
$translatedLevel)});
this.document.on("mousein", update{concat(local-name($data),
$translatedLevel)});
this.document.on("mouseout", update{concat(local-name($data),
$translatedLevel)});
}});
function update{concat(local-name($data), $translatedLevel)}()
{{
CKEDITOR.tools.setTimeout( function()
{{
editorData = CKEDITOR.instances['{local-name($data)}-
{$translatedLevel}-editor'].getData();
instance = document.getElementById
("contentObjectEntryEditInstance");
content = instance.getElementsByTagName("atom:content")
[0];
element = content.getElementsByTagName('{name($data)}')
[0];
if(element.childNodes[0]){{element.childNodes[0].nodeValue
= editorData;}}
else {{var elementTextNode = document.createTextNode
(editorData); element.appendChild(elementTextNode);}};
outputDiv = document.getElementById("output-{local-name
($data)}-{$translatedLevel}");
if(outputDiv.childNodes[0]){{outputDiv.childNodes
[0].nodeValue = element.childNodes[0].nodeValue}}
else {{var editorDataTextNode = document.createTextNode
(element.childNodes[0].nodeValue); outputDiv.appendChild
(editorDataTextNode);}};
var model = document.getElementById("mainModel");
model.rebuild();
model.recalculate();
model.refresh();
}}, 0);
}}
</script>

</div>

Thanks,

Fraser

John Boyer

unread,
Nov 16, 2009, 1:25:48 PM11/16/09
to ubiquit...@googlegroups.com

Hi Fraser,

The best way to get the model to update is to call model.deferredUpdate().

However, that's not the cause of your immediate problem since you're calling rebuild, recalculate, etc. manually.

Do you transfer the data from CKEditor to the model data using model.setValue()?
If it's just text content, then model.setValue() followed by model.deferredUpdate() ought to do the trick for you.

If you have to move element content, then you'd have to use functions that behave as if you're doing an xforms insert and/or delete action (or a sequence of the same), then followed by model.deferredUpdate().

In brief, you'd have to do model.getInstanceDocument() to get the instance into which you want to insert.  Then, have a look at insert.js as a guide for how to do an insertion. There's only two steps involved.  The first is calling oInstance.insertNodeset(), then you set the model.flagRebuild().  This second setting is what tells the deferredUpdate() that a rebuild (and all the follows) must be performed.

Cheers,
John M. Boyer, Ph.D.
STSM, Interactive Documents and Web 2.0 Applications
Chair, W3C Forms Working Group
Workplace, Portal and Collaboration Software
IBM Victoria Software Lab
E-Mail: boy...@ca.ibm.com  

Blog:
http://www.ibm.com/developerworks/blogs/page/JohnBoyer
Blog RSS feed:
http://www.ibm.com/developerworks/blogs/rss/JohnBoyer?flavor=rssdw




From: Fraser <frase...@gmail.com>
To: Ubiquity XForms Developers <ubiquit...@googlegroups.com>
Date: 11/16/2009 09:45 AM
Subject: [ubiquity-xforms] CKEditor almost working, just need to rebuild model


Fraser

unread,
Nov 17, 2009, 8:03:05 AM11/17/09
to Ubiquity XForms Developers
Thanks for the response. Unfortunately model.deferredUpdate() didn't
work even when I updated the model instance element with just simple
text. The following code sets the value of the instance element to
that of the edtor:

var editorData = CKEDITOR.instances['editor'].getData();
var model = document.getElementById("mainModel");
var instance = document.getElementById
("contentObjectEntryEditInstance");
var content = instance.getElementsByTagName("atom:content")
[0];
var element = content.getElementsByTagName('{name($data)}')
[0];
if(element.childNodes[0]){{element.childNodes[0].nodeValue
= editorData;}}
else {{var elementTextNode = document.createTextNode
(editorData); element.appendChild(elementTextNode);}};

Notice that I'm accessing the instance using: var instance =
document.getElementById("contentObjectEntryEditInstance");

because the getInstanceDocument doesn't seem to work for some reason:
instance = model.getInstanceDocument
("contentObjectEntryEditInstance");

Perhaps this hints at the problem. I don't know if this makes a
difference. I do know from the console and from making the model
visible that the instance element is definitely getting updated.

Would it make more sense to update the <pe-value> element instead of
the instance element? It would actually be a cleaner solution,
especially if Ubiquity would automatically detect the change to <pe-
value> and update the model. It may also be possible to have the
editor replace the <pe-value> element and then use CKEditor's own
update element method to update the <pe-value> element.

I experimented a little to try to access the <pe-value> element but
wasn't successful. It shows up when I inspect the xf:textarea in the
console but I don't seem to be able to access it either by
getElementsByTagName or by walking the childNodes.

Cheers,

Fraser

John Boyer

unread,
Nov 17, 2009, 12:45:28 PM11/17/09
to ubiquit...@googlegroups.com

Hi Fraser,

Actually, the first two things I said were

1) use model.deferredUpdate(),
2) this isn't your problem...

I point this out because you've responded by saying that you tried model.deferredUpdate() and it didn't work. But that is the second thing I had said, so it is necessary to proceed from there in order to get it to work.

The model.deferredUpdate() is just the catch-all for invoking (optionally) rebuild, recalculate, revalidate and refresh *if the update flags are set*.  You generally have to make less calls, and it also ensures you don't skip steps (e.g. you appear to be skipping the model revalidate step).

Whether you use model.deferredUpdate() or stick with calling things like rebuild and recalculate, the root of your problem is that you are making changes but then not setting the appropriate flags (rebuild or recalculate), so your calls aren't doing anything because nothing is flagged to do.

The rest of my email then went on to explain other functions you could/should use.

For setting simple text values, I mentioned model.setValue().  This function sets the value and then sets the appropriate flag, so that if you do call deferredUpdate(), then you would get the updated behavior.

For copying structural subtrees, I suggested using the model's getInstanceDocument() then using the instance's insertNodeset() method, then setting the rebuild flag (see insert.js for details), then calling deferredUpdate().

I think you are down the path of using getInstanceDocument(), even for simple text setting.  This is OK, since model.setValue() is a convenience function.  In this case, you'd have to use model.getInstanceDocument(), followed by setting the actual value (see model.setValue() for details), then be sure to set the recalculate flag before invoking deferredUpdate().

You asked whether it would be better to set the pe-value.  You might "get it to work", but no you should not do it that way.  Use of getInstanceDocument() is the only supported method. The pe-value is an internal implementation details, and we might (or might not) change how pe-value works in the future, and if so then your code will stop working.

Finally, you mentioned that you are having trouble with getInstanceDocument() to return something to you.  That is the real root of your problem, since using that function is the correct approach.  Not clear offhand why it doesn't work for you without more details, e.g. when is it being called.  For example, I would not expect getInstanceDocument() to return anything to you until after the completion of the xforms-model-construct event.  So, if you are invoking the function before that event processing has finished, then that would explain why you are not getting a result.  Ideally, since you are doing things in the UI, I would suggest that you hook the xforms-ready event to do your setup.  This is because xforms-ready is *the* event to hook for proper setup after the XForms user interface and UI bindings have been fully processed.  If you call model.getInstanceDocument() from that handler, it should work. Otherwise, the very earliest event where I would think getInstanceDocument() should work correctly if you call it from an event handler would be xforms-model-construct-done.  The xforms UI won't have been processed, but the instances and model binds are processed by that point (per XForms spec).

Cheers,
John M. Boyer, Ph.D.
STSM, Interactive Documents and Web 2.0 Applications
Chair, W3C Forms Working Group
Workplace, Portal and Collaboration Software
IBM Victoria Software Lab
E-Mail: boy...@ca.ibm.com  

Blog:
http://www.ibm.com/developerworks/blogs/page/JohnBoyer
Blog RSS feed:
http://www.ibm.com/developerworks/blogs/rss/JohnBoyer?flavor=rssdw




From: Fraser <frase...@gmail.com>
To: Ubiquity XForms Developers <ubiquit...@googlegroups.com>
Date: 11/17/2009 05:03 AM
Subject: [ubiquity-xforms] Re: CKEditor almost working, just need to rebuild model





Fraser

unread,
Nov 17, 2009, 11:10:25 PM11/17/09
to Ubiquity XForms Developers
Hi John,

Thanks so much for taking the time to explain, it was a big help! I
finally got it working. I'm pretty excited to have a good rich text
editor to use with Ubiquity XForms! Now if I can just get my
submissions to work I will have a pretty cool little xrx cms using
eXist and Ubiquity.

Here is the final working code for an XForms textarea with the
CKEditor:

xquery version "1.0";
(: Textarea Element Edit Template :)

declare namespace xhtml = "http://www.w3.org/1999/xhtml";
declare namespace atom = "http://www.w3.org/2005/Atom";
declare namespace xforms = "http://www.w3.org/2002/xforms";
declare namespace xf = "http://www.w3.org/2002/xforms";
declare namespace ev = "http://www.w3.org/2001/xml-events";
declare namespace cms = "http://www.mysite.org/cms";

(:
$level contains the outline level in the form of 1.1, 1.2, 1.1.1,
1.1.2 etc.
and is used as a unique identifier for the textarea element in the
javascript below. This identifier
will differentiate this textarea and it's associated javascript from
any other textarea elements on the page.
As an alternative random() or some other function could be used to
create a unique identifier
:)

let $translatedLevel := translate($level, '.', '')
return

<div>
<xf:textarea ref="{$ref}" incremental="true" id="{local-name
($data)}-{$translatedLevel}" class="editor">
<xf:label>{$level}. {content:getLabel(name($data),'',
$contentElementSchema)}</xf:label>
</xf:textarea>

<!--(: This output is here only to demonstrate that the xforms
instance element is being properly updated with the data from the
editor :)-->
<xf:output ref="{$ref}">
<xf:label>Instance Data</xf:label>
</xf:output>

<!--(:
This script inserts a wrapper div and an editor div inside of the
xf:textarea,
replaces the editor div with a CKEditor instance, updates the
CKEditor instance with
data from the xforms instance element and then waits for some
activity in the editor.
Upon specified actions in the editor, a function is called to
update the xforms instance
element wit hthe content of the editor and rebuild the xforms
model.
NOTE: getElementsByTagName is used at first to get data from the
xforms instance element
because when the script is first run during page load, the
getInstanceDocument method
isn't yet available because the Ubiquity xforms script is still
building the model.
When the update function is called, getInstanceDocument is
available and can be used to access
and update the instance element. Also note that element name used
in the evalXPah expression
has to be in lower case for some reason even though the actual
element name is in UpperCamel case.
:)-->
<script type="text/javascript">
var xfTextarea = document.getElementById( '{local-name($data)}-
{$translatedLevel}' );
var editorWrapper = document.createElement('div');
var editorWrapperStyle = editorWrapper.style.clear = 'both';
var editorDiv = document.createElement('div');
var editorDivID = editorDiv.setAttribute( 'id', '{local-name
($data)}-{$translatedLevel}-editor' );
editorWrapper.appendChild(editorDiv);
xfTextarea.appendChild(editorWrapper);
var editor = CKEDITOR.replace( '{local-name($data)}-
{$translatedLevel}-editor' );
var instance = document.getElementById
("contentObjectEntryEditInstance");
var content = instance.getElementsByTagName("atom:content")[0];
var element = content.getElementsByTagName( '{name($data)}' )
[0];
if(element.childNodes[0]){{editor.setData( element.childNodes
[0].nodeValue )}};
editor.on("instanceReady", function()
{{
this.document.on("keyup", update{concat(local-name($data),
$translatedLevel)});
this.document.on("paste", update{concat(local-name($data),
$translatedLevel)});
this.document.on("mousein", update{concat(local-name($data),
$translatedLevel)});
this.document.on("mouseout", update{concat(local-name($data),
$translatedLevel)});
}});
function update{concat(local-name($data), $translatedLevel)}()
{{
CKEDITOR.tools.setTimeout( function()
{{
var editorData = CKEDITOR.instances['{local-name($data)}-
{$translatedLevel}-editor'].getData();
var model = document.getElementById("mainModel");
var instance = model.getInstanceDocument
("contentObjectEntryEditInstance");
var element = instance.XFormsInstance.evalXPath("//{lower-
case(name($data))}").value[0];
if(element.childNodes[0]){{element.childNodes[0].nodeValue
= editorData;}}
else {{var elementTextNode = document.createTextNode
(editorData); element.appendChild(elementTextNode);}};
model.flagRebuild();
model.deferredUpdate();
}}, 0);
}}
</script>

</div>

Fraser

unread,
Nov 18, 2009, 12:18:56 PM11/18/09
to Ubiquity XForms Developers
Just a few thoughts on the code. The reason I am inserting new div
inside the xf:textarea and replacing them with the editor is that it
allows the xf:textarea label to still do it's job. Also, I've put in
a listener for key up, which is effectively like setting the
xf:textarea to incremental="true". This does slow down data entry a
bit. Unfortunately I couldn't get the DOMfocusout event to work which
would be like incremental="false" for the default for the normal
xf:textarea. In the future it would be good to get this or some other
event that achieves the same result to work, then we could make that
the default, and listen for the keyup event if the increment attribute
is set to "true".

Right now this code needs to be placed after the xf:textarea in the
body of the page, and it is convenient for me this way as I can re-use
the same variables I use to dynamically generate the xf:textarea in
the first place, such as $data which is the xml element in the
instance. However, it would probably be better to create a script
that can be activated upon xforms-ready to iterate through all of the
xf:textareas that have a particular class value (i.e. editor) and set
up the listener and the call to the update function. The update
function could be generalized by passing it parameters for the model,
instance and xpath to the element to be updated, as well as the name
of the editor from which the data should be retrieved. Perhaps this
could be achieved by finding an xf:textarea, retrieving its ref or
bind attribute and then walking up the parent nodes until you have the
complete xpath. I don't have time to work on this right now, and I'm
not a javascript expert anyway but if someone wanted to give it a try
that would be cool! Also if anyone sees some opportunities to make
this code more efficient that would be great also. I hope others find
this useful.

Cheers,

Fraser

John Boyer

unread,
Nov 19, 2009, 1:14:41 PM11/19/09
to ubiquit...@googlegroups.com

Hi Fraser,

While I haven't had the time to exhaustively review what you're doing, I think the DOMFocusOut is not working for you because you're doing something with the xf:textarea that is a little bit... suboptimal.  There's not a great fix for it quite yet, but the group does know about the need to do the type of thing you're doing in a "supported" way.

Basically, you're hunting for xf:textarea and creating your own control for it.  The problem is, I think, that the ubiquity processor's decorator is also looking for xf:textarea and applying one of *our* textarea controls to it.  I think your control is overlaying ours, so you can see and interact with yours, but some of the important UI interactions that ubiquity implements are not recognizing your control.

The right fix for this would be for you to be able to provide your own decorator rule that would take precedence over ours.  We don't explicitly support doing that right now.  Moreover, I'm sure there's a little more to it than that.  I think it is not too hard, but I didn't write the calendar control, for example, so I don't know the additional details offhand.

Anyway, I hope this helps at least explain a bit about why it's not doing quite what you want right now.

Cheers,
John M. Boyer, Ph.D.
STSM, Interactive Documents and Web 2.0 Applications
Chair, W3C Forms Working Group
Workplace, Portal and Collaboration Software
IBM Victoria Software Lab
E-Mail: boy...@ca.ibm.com  

Blog:
http://www.ibm.com/developerworks/blogs/page/JohnBoyer
Blog RSS feed:
http://www.ibm.com/developerworks/blogs/rss/JohnBoyer?flavor=rssdw




From: Fraser <frase...@gmail.com>
To: Ubiquity XForms Developers <ubiquit...@googlegroups.com>
Date: 11/18/2009 09:19 AM
Subject: [ubiquity-xforms] Re: CKEditor almost working, just need to rebuild model





Reply all
Reply to author
Forward
0 new messages