CKEditor: multiple widget templates - ckeditor

I'm currently creating a 'smartobject' widget. In the widgets dialog, the user can choose a 'smartobject', which simply put, generates some html, which should be added to the editor. Here comes the tricky part: the html sometimes div elements and sometimes simply span elements. In the case of the div variant, the widget should be wrapped in a div 'template'. In the case of a span variant, the widget should be wrapped in a span and the html should be added 'inline'.
In the widgets API I see the following way to define a template:
editor.widgets.add('smartobject', {
dialog: 'smartobject',
pathName: lang.pathName,
template: '<div class="cke_smartobject"></div>', // <------
upcast: function(element) {
return element.hasClass('smartObject');
},
init: function() {
this.setData('editorHtml', this.element.getOuterHtml());
},
data: function() {
var editorHtml = this.data.editorHtml;
var newElement = new CKEDITOR.dom.element.createFromHtml(editorHtml);
newElement.copyAttributes(this.element);
this.element.setText(newElement.getText());
}
});
But in my case, the template is more dynamic: sometimes a div and sometimes the span will do the correct thing..
How can I fix this without needing to create two widgets which will do the exact same thing, with only the wrapping element as difference?
I've already tried to replace the entire element in the 'data' method, like:
newElement.replace(this.element);
this.element = newElement;
But this seemed not supported: resulted in undefined errors after calling editor.getData().
I'm using ckeditor v4.5.9
Thanks for your help!

It seems I got it working (with a workaround).
The code:
CKEDITOR.dialog.add('smartobject', this.path + 'dialogs/smartobject.js');
editor.widgets.add('smartobject', {
pathName: lang.pathName,
// This template is needed, to activate the widget logic, but does nothing.
// The entire widgets html is defined and created in the dialog.
template: '<div class="cke_smartobject"></div>',
init: function() {
var widget = this;
widget.on('doubleclick', function(evt) {
editor.execCommand('smartobject');
}, null, null, 5);
},
upcast: function(element) {
return element.hasClass('smartObject');
}
});
// Add a custom command, instead of using the default widget command,
// otherwise multiple smartobject variants (div / span / img) are not supported.
editor.addCommand('smartobject', new CKEDITOR.dialogCommand('smartobject'));
editor.ui.addButton && editor.ui.addButton('CreateSmartobject', {
label: lang.toolbar,
command: 'smartobject',
toolbar: 'insert,5',
icon: 'smartobject'
});
And in the dialog, to insert code looks like:
return {
title: lang.title,
minWidth: 300,
minHeight: 80,
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
editor.insertElement(element);
// Trigge the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
},
...etc.
UPDATE
I made the 'onOk' method a little bit better: the smartobject element is now selected after the insertion.
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
var elementId = "ckeditor-element-" + element.getUniqueId();
element.setAttribute("id", elementId);
editor.insertElement(element);
// Trigger the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
// Get the element 'fresh' by it's ID, because the setData method,
// makes the element change into a widget, and thats the element which should be selected,
// after adding.
var refreshedElement = CKEDITOR.document.getById(elementId);
var widgetWrapperElement = CKEDITOR.document.getById(elementId).getParent();
// Better safe then sorry: if the fresh element doesn't have a parent, simply select the element itself.
var elementToSelect = widgetWrapperElement != null ? widgetWrapperElement : refreshedElement;
// Normally the 'insertElement' makes sure the inserted element is selected,
// but because we call the setData method (to ensure the element is transformed to a widget)
// the selection is cleared and the cursor points to the start of the editor.
editor.getSelection().selectElement(elementToSelect);
},
So in short, I partially used the widget API for the parts I wanted:
- Make the html of the widget not editable
- Make it moveable
But I created a custom dialog command, which simply bypasses the default widget insertion, so I can entirely decide my own html structure for the widget.
All seems to work like this.
Any suggestions, to make it better are appreciated:)!

As suggested in this ckeditor forum thread, the best approach would be to set the template to include all possible content elements. Then, in the data function, remove the unnecessary parts according to your specific logic.

Related

How to force CKEditor to process widgets when setting the HTML of an element

I have create a simple CKEditor widget that highlights the elements that have the class "pink".
I have also added a "Pinkify" button to the toolbar, which replaces the HTML of the selected element with some other elements that have the class "pink".
What I observe when I click the button is that widgets are not created for the freshly inserted elements. However, when I toggle between Source mode and WYSISYG mode, the widgets get created.
See the jsfiddle and its code:
CKEDITOR.replace('ck', {
allowedContent: true,
extraPlugins: 'pink'
});
CKEDITOR.plugins.add('pink', {
requires: 'widget',
init: function(editor) {
editor.widgets.add('pinkwidget', {
upcast: function(element) {
return element.hasClass('pink');
}
});
editor.addCommand('pinkify', {
editorFocus: 1,
exec: function(editor) {
var selection = editor.getSelection(),
selectedElement = selection.getStartElement();
if (selectedElement) {
selectedElement.setHtml("Let's have some <span class=\"pink\">pink</span> widget here!");
editor.widgets.checkWidgets(); // needed?
}
}
});
editor.ui.addButton('pinkify', {
label: 'Pinkify',
command: 'pinkify'
});
},
onLoad: function() {
CKEDITOR.addCss('.cke_widget_pinkwidget { background: pink; }');
}
});
I am aware of this question on Stackoverflow, but I can't get it to work with setHtml called on an element. Can you suggest how to modify the code so that widgets get created as soon as the HTML is updated?
According to the CKEditor team, it is normal that CKEDITOR.dom.element.setHtml does not instanciate widgets (see Widgets not initialised after calling setHtml on an element).
So the workaround they gave me was to rewrite the code that insert HTML in place of the selected element to:
if (selectedElement) {
selectedElement.setHtml("");
editor.insertHtml("Let's have some <span class=\"pink\">pink</span> widget here!");
}
For those like me who didn't know, editor.insertHTML inserts HTML code into the currently selected position in the editor in WYSIWYG mode.
Updated jsFiddle here.

Add or trigger event after inner content to page

I have links on a table to edit or delete elements, that elements can be filtered. I filtered and get the result using ajax and get functions. After that I added (display) the result on the table using inner.html, the issue here is that after filtering the links on the elements not work, cause a have the dojo function like this
dojo.ready(function(){
dojo.query(".delete-link").onclick(function(el){
var rowToDelete = dojo.attr(this,"name");
if(confirm("Really delete?")){
.......
}
});
I need to trigger the event after filtering, any idea?
(I'm assuming that you're using Dojo <= 1.5 here.)
The quick answer is that you need to extract the code in your dojo.ready into a separate function, and call that function at the end of your Ajax call's load() callback. For example, make a function like this:
var attachDeleteEvents = function()
dojo.query(".delete-link").onclick(function(el){
var rowToDelete = dojo.attr(this,"name");
if(confirm("Really delete?")){
.......
}
});
};
Then you call this function both in dojo.ready and when your Ajax call completes:
dojo.ready(function() { attachDeleteEvents(); });
....
var filter = function(someFilter) {
dojo.xhrGet({
url: "some/url.html?filter=someFilter",
handleAs: "text",
load: function(newRows) {
getTableBody().innerHTML = newRows;
attachDeleteEvents();
}
});
};
That was the quick answer. Another thing that you may want to look into is event delegation. What happens in the code above is that every row gets an onclick event handler. You could just as well have a single event handler on the table itself. That would mean there would be no need to reattach event handlers to the new rows when you filter the table.
In recent versions of Dojo, you could get some help from dojo/on - something along the lines of:
require(["dojo/on"], function(on) {
on(document.getElementById("theTableBody"), "a:click", function(evt) {...});
This would be a single event handler on the whole table body, but your event listener would only be called for clicks on the <a> element.
Because (I'm assuming) you're using 1.5 or below, you'll have to do it a bit differently. We'll still only get one event listener for the whole table body, but we have to make sure we only act on clicks on the <a> (or a child element) ourselves.
dojo.connect(tableBody, "click", function(evt) {
var a = null, name = null;
// Bubble up the DOM to find the actual link element (which
// has the data attribute), because the evt.target may be a
// child element (e.g. the span). We also guard against
// bubbling beyond the table body itself.
for(a = evt.target;
a != tableBody && a.nodeName !== "A";
a = a.parentNode);
name = dojo.attr(a, "data-yourapp-name");
if(name && confirm("Really delete " + name + "?")) {
alert("Will delete " + name);
}
});
Example: http://fiddle.jshell.net/qCZhs/1/

AJAX content in a jQuery UI Tooltip Widget

There is a new Tooltip Widget in jQuery UI 1.9, whose API docs hint that AJAX content can be displayed in it, but without any further details. I guess I can accomplish something like that with a synchronous and blocking request, but this isn't what I want.
How do I make it display any content that was retrieved with an asynchronous AJAX request?
Here is a ajax example of jqueryui tootip widget from my blog.hope it helps.
$(document).tooltip({
items:'.tooltip',
tooltipClass:'preview-tip',
position: { my: "left+15 top", at: "right center" },
content:function(callback) {
$.get('preview.php', {
id:id
}, function(data) {
callback(data); //**call the callback function to return the value**
});
},
});
This isn't a complete solution obviously, but it shows the basic technique of getting data dynamically during the open event:
$('#tippy').tooltip({
content: '... waiting on ajax ...',
open: function(evt, ui) {
var elem = $(this);
$.ajax('/echo/html').always(function() {
elem.tooltip('option', 'content', 'Ajax call complete');
});
}
});
See the Fiddle
One thing to lookout for when using the tooltip "content" option to "AJAX" the text into the tooltip, is that the text retrieval introduces a delay into the tooltip initialization.
In the event that the mouse moves quickly across the tooltip-ed dom node, the mouse-out event may occur before the initialization has completed, in which case the tooltip isn't yet listening for the event.
The result is that the tooltip is displayed and does not close until the mouse is moved back over the node and out again.
Whilst it incurs some network overhead that may not be required, consider retrieving tooltip text prior to configuring the tooltip.
In my application, I use my own jquery extensions to make the AJAX call, parse the resutls and initialise ALL tooltips, obviously you can use jquery and/or your own extensions but the gist of it is:
Use image tags as tooltip anchors, the text to be retrieved is identified by the name atrribute:
<img class="tooltipclassname" name="tooltipIdentifier" />
Use invoke extension method to configure all tooltips:
$(".tooltipclassname").extension("tooltip");
Inside the extension's tooltip method:
var ids = "";
var nodes = this;
// Collect all tooltip identifiers into a comma separated string
this.each(function() {
ids = ids + $(this).attr("name") + ",";
});
// Use extension method to call server
$().extension("invoke",
{
// Model and method identify a server class/method to retrieve the tip texts
"model": "ToolTips",
"method": "Get",
// Send tooltipIds parameter
"parms": [ new myParmClass("tipIds", ids ) ],
// Function to call on success. data is a JSON object that my extension builds
// from the server's response
"successFn": function(msg, data) {
$(nodes).each(function(){
// Configure each tooltip:
// - set image source
// - set image title (getstring is my extension method to pull a string from the JSON object, remember that the image's name attribute identifies the text)
// - initialise the tooltip
$(this).attr("src", "images/tooltip.png")
.prop("title", $(data).extension("getstring", $(this).attr("name")))
.tooltip();
});
},
"errorFn": function(msg, data) {
// Do stuff
}
});
// Return the jquery object
return this;
Here is an example that uses the jsfiddle "/echo/html/" AJAX call with a jQuery UI tooltip.
HTML:
<body>
<input id="tooltip" title="tooltip here" value="place mouse here">
</body>
JavaScript:
// (1) Define HTML string to be echo'ed by dummy AJAX API
var html_data = "<b>I am a tooltip</b>";
// (2) Attach tooltip functionality to element with id == tooltip
// (3) Bind results of AJAX call to the tooltip
// (4) Specify items: "*" because only the element with id == tooltip will be matched
$( "#tooltip" ).tooltip({
content: function( response ) {
$.ajax({
url: "/echo/html/",
data: {
'html': html_data
},
type: "POST"
})
.then(function( data ) {
response( data );
});
},
items: "*"
});
here is this example on jsfiddle:

jquery: bind click event to ajax-loaded elmente? live() won't work?

hey guys,
I have an input field that looks for matched characters on a page. This page simply lists anchor links. So when typing I constantly load() (using the jquery load() method) this page with all the links and I check for a matched set of characters. If a matched link is found it's displayed to the user. However all those links should have e.preventDefault() on them.
It simply won't work. #found is the container that shows the matched elements. All links that are clicked should have preventDefault() on them.
edit:
/*Animated scroll for anchorlinks*/
var anchor = '',
pageOffset = '',
viewOffset = 30,
scrollPos = '';
$(function() {
$("a[href*='#']").each(function() {
$(this).addClass('anchorLink');
$(this).bind('click', function(e) {
e.preventDefault();
//console.log('test');
anchor = $(this).attr('href').split('#')[1];
pageOffset = $("#"+anchor).offset();
scrollPos = pageOffset.top - viewOffset;
$('html, body').animate({scrollTop:scrollPos}, '500');
})
});
});
Well, I'm looking for all href's that contain a #. So I know those elements are anchors that jump to other elements. I don't want my page to jump, but rather scroll smoothly to this element with this specific #id.
This works fine when I use bind('click', ... for normal page-elements that have been loaded when the page is opened. It doesn't work for anchors that have been loaded via ajax! If I change the bind to live nothing does change for the ajax loaded elements - they still don't work. However normal anchors that have always been on the page are not triggering the function as well. So nothing works with live()!
When you say "it won't work" do you mean that your function is not been called or that you can not cancel out of the function? As far as I know you can not cancel out live events. With jQuery 1.4 you can use return false to cancel out live event propagation. Calling e.preventDefault() won't work.
Edit: right so this code should in principal work. What it still won't do is, it won't add the 'anchorLink' class to your new anchors. However if the clicks work then let me know and I will give you the right code to add the class too!
var anchor = '',
pageOffset = '',
viewOffset = 30,
scrollPos = '';
$(function() {
$("a[href*='#']").each(function() {
$(this).addClass('anchorLink');
});
$("a").live('click', function(e) {
if ($(this).attr("href").indexOf("#") > -1) {
e.preventDefault();
//console.log('test');
anchor = $(this).attr('href').split('#')[1];
pageOffset = $("#" + anchor).offset();
scrollPos = pageOffset.top - viewOffset;
$('html, body').animate({ scrollTop: scrollPos }, '500');
//nikhil: e.preventDefault() might not work, try return false here
}
});
});

prototype get inner html of multiple classes

I am new to prototype and finding it a lot more difficult than jquery. All i want to do is get the inner html of various classes.
$$('.book').each(function() {
var msg = this.down(".information");
alert(msg.innerHTML);
//new Tip(this, msg.innerHTML, {stem: 'topLeft', hook: { tip: 'topLeft', mouse: true }, offset: { x: 14, y: 14 }});
});
I'm trying to create tooltips for multiple items, but I'm not even getting the alert.
I think you can probably prevent the extra dom work of down() like this:
$$('.book .information').each(function(book) {
alert(book.innerHTML);
});
remember you also have the ability to use advanced CSS2 and CSS3 selectors in prototype like this for example:
$$('.book a[rel]').each(function(el) {
alert(el.rel);
});
see the bottom of this page for more examples http://www.prototypejs.org/api/utility/dollar-dollar
The this variable is not pointing to the element you're iterating over in Prototype, you have to explicitly use a parameter:
$$('.book').each(function(book) {
var msg = book.down(".information");
alert(msg.innerHTML);
});

Resources