PDA

View Full Version : HtmlEditor Styles Plugin



dangreenfield
24 Nov 2007, 2:30 AM
I am currently working on a plugin to allow the selection of a Style from a combo box on the HtmlEditor Toolbar that will apply it to the selected range. I realise that the browsers don't currently support this through their RTE controls and that I will need to create the feature programatically. I will put my code in this forum entry as I develop it, along with any issues and requests for help that I can't find addressed elsewhere.

The first issue I'd like to raise is a modification request to the Toolbar code. I'm not currently a premium member, nor can I afford to be one right now, although I intend to be one in the near future, so I'll put the request here and hope that it gets viewed.

The ability to insert buttons on the toolbar, as opposed to adding them to the end, is a must if the HtmlEditor is to be a truly plugin-friendly tool, which Jack has expressed is his intention. Although all this needed code could actually be included in the plugin itself, it seems to me that the correct place would actually be within the toolbar itself. I realise that the current desire is that the code not be bulky, like other editor offerings, but at the very least, it should be capable of supporting plugins fully, making it easy on those who wish to extend it.

I notice that the toolbar code caters for the insert of a button (Ext.Toolbar.insertButton), but nothing else has been included. I thought the first start would be to have an insertItem function, and then for the add... functions to use insertItem instead of the addItem if a valid insert index is passed, such as what follows...


insertItem: function(index, item) {
var td = document.createElement("td");
this.tr.insertBefore(td, this.tr.childNodes[index]);
this.initMenuTracking(item);
item.render(td);
this.items.insert(index, item);
return item;
}


However, I realised that not all add... functions used addItem and several had their own add routines. The next best idea, I felt, was to have each individual add... function cater for the insert index as the first (or only) parameter and if the index value == -1 then it performed an add, otherwise it performed an insert, similar to what follows...


addItem: function(index, item) {
// var td = this.nextBlock(); <-- old code
var td = document.createElement("td");
if (index < 0) {
this.tr.appendChild(td);
} else {
this.tr.insertBefore(td, this.tr.childNodes[index]);
}
this.initMenuTracking(item);
item.render(td);
// this.items.add(item); <-- old code
// code below would be shorter if MixedCollection.insert
// accepted an index of -1 to mean add
if (index < 0) {
this.items.add(item);
} else {
this.items.insert(index, item);
}
return item;
},


The last problem arises when you look at the general Ext.Toolbar.add function, which only takes an array of item objects, and leaves no room for an insert index for each item. My suggestion is to modify the code to cater for a new xtype of 'indexed' (or something more appropriate), so that the user can pass a wrapper object holding the item object and the insert index (i.e, {xtype: "indexed", index: 0, item: "-"}).


add: function() {
var a = arguments, l = a.length;
for (var i = 0; i < l; i++) {
var el = a[i];
var index = -1;
if (typeof el == "object" && el.xtype && el.xtype == "indexed") {
index = el.index;
el = el.item;
}
if (el.isFormField) { // some kind of form field
this.addField(index, el);
} else if (el.render) { // some kind of Toolbar.Item
this.addItem(index, el);
} else if (typeof el == "string") { // string
if (el == "separator" || el == "-") {
this.addSeparator(index);
} else if (el == " ") {
this.addSpacer(index);
} else if (el == "->") {
this.addFill(index);
} else {
this.addText(index, el);
}
} else if(el.tagName) { // element
this.addElement(index, el);
} else if (typeof el == "object") { // must be button config?
if (el.xtype) {
this.addField(index, Ext.ComponentMgr.create(el, 'button'));
} else {
this.addButton(index, el);
}
}
}
},


The reason why I want this flexibility is because the Styles combo box simply looks and feels best placed before the Font Family combo box.

My plugin currently only adds the combo box, which is a perfect start. From here on out, I'll be working on the functionality.


Ext.ux.HTMLEditorStyles = function(cfg) {
cfg = cfg || [];
if (! cfg instanceof Array) {
cfg = [cfg];
}
cfg = {styles: [{name: "No Style", value: ""}].concat(cfg)};
Ext.apply(this, cfg, {enableStyle: true});
this.init = function(htmlEditor) {
this.editor = htmlEditor;
this.editor.on('render', onRender, this);
};
this.createStyleOptions = function() {
var styles = this.styles;
var buf = [];
for (var i = 0, len = styles.length; i < len; i++) {
style = styles[i];
buf.push(
'<option value="', style.value.toLowerCase(), '">', style.name, '</option>'
);
}
return buf.join('');
};
this.insertItem = function(index, item) {
var tb = this.editor.tb;
var td = document.createElement("td");
tb.tr.insertBefore(td, tb.tr.childNodes[index]);
tb.initMenuTracking(item);
item.render(td);
tb.items.insert(index, item);
}
function onRender() {
if (this.enableStyle && ! Ext.isSafari) {
this.styleSelect = this.editor.tb.el.createChild({
tag: 'select',
cls: 'x-font-select',
html: this.createStyleOptions()
});
this.styleSelect.on('change', function() {
var style = this.styleSelect.dom.value;
// styling code goes here
}, this);
this.insertItem(0, new Ext.Toolbar.Spacer());
this.insertItem(0, new Ext.Toolbar.Item(this.styleSelect.dom));
}
}
}


The plugin is added to the HtmlEditor config as follows.


plugins: new Ext.ux.HTMLEditorStyles([
{name: "Section Heading", value: "section_head"}
])


Let me know if anything obvious sticks out on how I'm coding this. I'll add the functionality once it's complete, or as issues arise.

dangreenfield
26 Nov 2007, 1:59 AM
Here's the code for the completed HtmlEditor Styles Plugin. It was simpler than I thought it would be. As there were several differences in the way that IE and Mozilla worked with selections, I ended up creating two different functions, one for each browser.


Ext.ux.HTMLEditorStyles = function(cfg) {
cfg = cfg || [];
if (! cfg instanceof Array) {
cfg = [cfg];
}
cfg = {styles: [{name: "No Style", value: "none"}].concat(cfg)};
Ext.apply(this, cfg, {enableStyle: true});
this.init = function(htmlEditor) {
this.editor = htmlEditor;
this.editor.on('render', onRender, this);
};
this.createStyleOptions = function() {
var styles = this.styles;
var buf = [];
for (var i = 0, len = styles.length; i < len; i++) {
style = styles[i];
buf.push(
'<option value="', style.value.toLowerCase(), '">', style.name, '</option>'
);
}
return buf.join('');
};
function getParentStyleElement(element) {
if (element) {
if (element.nodeType == 1 && element.tagName.toLowerCase() == "span" &&
element.className != "") {
return element;
}
else {
return getParentStyleElement(element.parentNode);
}
}
}
function doStyleIE() {
function removeStyle(element) {
element.removeAttribute('className');
var clone = element.cloneNode(false);
if (clone.outerHTML.toLowerCase() == "<span></span>") {
element.insertAdjacentHTML('beforeBegin', element.innerHTML);
element.parentNode.removeChild(element);
}
}
function removeChildStyle(element) {
for (var i = 0; i < element.children.length; i++) {
var child = element.children[i];
if (child.nodeType == 1) {
removeChildStyle(child);
if (child.tagName.toLowerCase() == "span") {
removeStyle(child);
}
}
}
}
var selection = this.editor.doc.selection;
var range = selection.createRange();
var style = this.styleSelect.dom.value;
if (style == "none") {
var element = range.parentElement();
var parent = getParentStyleElement(element);
removeStyle(parent);
}
else {
if (selection.type == "Text") {
var element = document.createElement("span");
element.innerHTML = range.htmlText;
element.className = style;
removeChildStyle(element);
range.pasteHTML(element.outerHTML);
}
}
this.editor.updateToolbar();
this.editor.deferFocus();
}
function doStyleGecko() {
function removeStyle(element) {
element.removeAttribute('class');
var wrapper = document.createElement("span");
wrapper.appendChild(element.cloneNode(false));
if (wrapper.innerHTML.toLowerCase() == "<span></span>") {
var fragment = document.createDocumentFragment() ;
for (var i = 0; i < element.childNodes.length; i++) {
fragment.appendChild(element.childNodes[i].cloneNode(true));
}
element.parentNode.replaceChild(fragment, element);
}
}
function removeChildStyle(element) {
for (var i = 0; i < element.childNodes.length; i++) {
var child = element.childNodes[i];
if (child.nodeType == 1) {
removeChildStyle(child);
if (child.tagName.toLowerCase() == "span") {
removeStyle(child);
}
}
}
}
var selection = this.editor.win.getSelection();
var style = this.styleSelect.dom.value;
if (style == "none") {
var element = selection.anchorNode;
var parent = getParentStyleElement(element);
removeStyle(parent);
}
else {
if (! selection.isCollapsed) {
var element = document.createElement("span");
for (var i = 0; i < selection.rangeCount; i++) {
element.appendChild(selection.getRangeAt(i).extractContents());
}
element.className = style;
removeChildStyle(element);
selection.getRangeAt(0).insertNode(element);
}
}
this.editor.updateToolbar();
this.onEditorEvent();
this.editor.deferFocus();
}
this.insertItem = function(index, item) {
var tb = this.editor.tb;
var td = document.createElement("td");
tb.tr.insertBefore(td, tb.tr.childNodes[index]);
tb.initMenuTracking(item);
item.render(td);
tb.items.insert(index, item);
}
function onRender() {
if (this.enableStyle && ! Ext.isSafari) {
this.styleSelect = this.editor.tb.el.createChild({
tag: 'select',
cls: 'x-font-select',
html: this.createStyleOptions()
});
this.styleSelect.on('change', Ext.isIE ? doStyleIE : doStyleGecko, this);
this.insertItem(0, new Ext.Toolbar.Spacer());
this.insertItem(0, new Ext.Toolbar.Item(this.styleSelect.dom));
Ext.EventManager.on(this.editor.doc, 'click', this.onEditorEvent, this);
Ext.EventManager.on(this.editor.doc, 'keyup', this.onEditorEvent, this);
}
}
this.onEditorEvent = function() {
var element = Ext.isIE ? this.editor.doc.selection.createRange().parentElement() :
this.editor.win.getSelection().anchorNode;
var parent = getParentStyleElement(element);
var style = parent ? parent.className : "none";
if (this.styleSelect.dom.value != style) {
this.styleSelect.dom.value = style;
}
}
}


Let me know if I've missed something, or even if you feel to correct my technique or style. I don't mind.

dangreenfield
2 Dec 2007, 5:18 PM
I have since created an extension to the Ext.form.HTMLEditor component (see http://extjs.com/forum/showthread.php?t=19480) to allow for easier integration of plugins. It follows that I would also rewrite this plugin to go with the new extension. It is attached.

Babysittah
2 Dec 2007, 9:30 PM
Nice:)

dangreenfield
14 Mar 2008, 12:46 PM
I have setup a live demo of this plugin. You can view it here (http://coder.4realhost.com/htmleditor/).

craigharmonic
17 Mar 2008, 10:56 PM
I have developed a style combo box that displays the style in the drop down list. I did it as a way to learn a bit more about the combo object after reading the icon extension tutorial (so it probably needs some work). Perhaps this would be a useful feature to add into your style selector?



/**
* Ext.ux.CSSCombo Extension Class for Ext 2.x Library
*
* @author Craig Harman
* @version $Id: Ext.ux.CSSCombo.js 1 2008-03-17 11:29:56Z
*
* @class Ext.ux.CSSCombo
* @extends Ext.form.ComboBox
*/
Ext.ux.CSSCombo = Ext.extend(Ext.form.ComboBox, {
initComponent:function() {

Ext.apply(this, {
tpl: '<tpl for=".">'
+ '<div class="x-combo-list-item '
+ '{' + this.cssField + '}">'
+ '{' + this.displayField + '}'
+ '</div></tpl>'
});

// call parent initComponent
Ext.ux.CSSCombo.superclass.initComponent.call(this);

}, // end of function initComponent

onRender:function(ct, position) {
// call parent onRender
Ext.ux.CSSCombo.superclass.onRender.call(this, ct, position);

// adjust styles
this.wrap.applyStyles({position:'relative'});

}, // end of function onRender

settCSS:function() {
var rec = this.store.query(this.valueField, this.getValue()).itemAt(0);
// Remove the current style(s) from the input field - this is probably a bad way to do it.
var theElement = this.el;
this.store.each(function(record) {
record.fields.each(function(field)
{
theElement.removeClass(record.get(field.name));
});
}, this);

// Add the class of the selected style to the input field
if(rec) {
this.el.addClass(rec.get(this.cssField));
}
}, // end of function settCSS

setValue: function(value) {
Ext.ux.CSSCombo.superclass.setValue.call(this, value);
this.settCSS();
} // end of function setValue
});

// register xtype
Ext.reg('CSSCombo', Ext.ux.CSSCombo);

// end of file


You can then use it as follows (with .redFont and .boldedFont defined in your CSS):



Ext.BLANK_IMAGE_URL = 'lib/ext/resources/images/default/s.gif';
Ext.onReady(function() {
var win = new Ext.Window({
title:'CSS Combo Ext 2.0 Extension Class Example',
width:400,
height:300,
layout:'form',
bodyStyle:'padding:10px',
labelWidth:70,
defaults:{anchor:'100%'},
items:[{
xtype:'CSSCombo',
fieldLabel:'CSSCombo',
store: new Ext.data.SimpleStore({
fields: ['style'],
data: [
['boldedFont'],
['redFont']
]
}),
valueField: 'style',
displayField: 'style',
cssField: 'style',
triggerAction: 'all',
mode: 'local'
}]
});
win.show();
});

dangreenfield
18 Mar 2008, 1:48 PM
Thanks Craig, that's a good idea.

It would need to have the ability to display text that is different (more verbose) to the classname of the style, and to have the style appear as normal text once its selected.

I would need to look into this a bit further when I have the time. Thanks again for the help.

craigharmonic
18 Mar 2008, 3:45 PM
No problem. Let me know how you go with it.

sunjoo
11 May 2008, 5:36 AM
Hi craigharmonic,

I am thinking of using style selector and have some questions please

1) does that show all the styles from a page with like this css,

<link rel="stylesheet" href="images/CitrusIsland.css" type="text/css" />


2) does that support @ style as well with multiple style sheets (all external css)

Thanks for the advice in advance.

Cheers
SunJoo

craigharmonic
11 May 2008, 4:38 PM
Hi craigharmonic,

I am thinking of using style selector and have some questions please

1) does that show all the styles from a page with like this css,

<link rel="stylesheet" href="images/CitrusIsland.css" type="text/css" />


2) does that support @ style as well with multiple style sheets (all external css)

Thanks for the advice in advance.

Cheers
SunJoo

Hi,

My style selector is simply a combo box with styles applied to the items inside, it currently doesn't have any functionality as neither myself or dan (who wrote the CSS combo drop down) have had time to implement each others code.

Cheers,

Craig

scipio
15 May 2008, 12:00 PM
Dan, you da man.

Great plugin. You saved me a ton of work.

MatjazH
19 Mar 2009, 11:46 PM
if (! selection.isCollapsed) {
// var element = document.createElement("span");
var element = selection.anchorNode.ownerDocument.createElement("span");


// if (! Ext.isSafari) {


// }