PDA

View Full Version : Ext.ux.TinyMCE adaption for Ext4



bgerth
9 Aug 2011, 4:02 AM
Hi forum members,

my supervisors recently asked me to evaluate whether it's possible to use the TinyMCE editor within an Ext4 form. Unfortunately I couldn't find any Ext4 resource for this, therefore I adapted the Ext3 extension (http://www.sencha.com/learn/legacy/Extension:TinyMCE).

Since I am an Ext beginner, I would like to share my result with the community to gather improvement ideas. Alternatively it would be nice if the developer of the Ext3 extension, Andrew Mayorov, would take over the maintenance of this code ;).

27377

Below you can find the extension code and under it a sample form.

Thanks,
Björn



/** ************************************************************
Ext.ux.TinyMCE v0.8.5
ExtJS form field containing TinyMCE v3.

Author: Andrew Mayorov et al.
http://blogs.byte-force.com/xor

Copyright (c)2008-2010 BYTE-force
www.byte-force.com

License: LGPLv2.1 or later
*/

// ***** Adaption for Ext4 *****

(function() {

var tmceInitialized = false;

// Lazy references to classes. To be filled in the initTinyMCE method.
var WindowManager,
ControlManager;

// Create a new Windows Group for the dialogs
/*var windowGroup = new Ext.WindowGroup();
windowGroup.zseed = 12000;*/


/**
* @class Ext.ux.TinyMCE
* @extends Ext.form.field.Text
*
* #Validation#
* Has the same validations as its parent class, {@link Ext.form.field.Text}.
*/
Ext.define('Ext.ux.TinyMCE', {
extend: 'Ext.form.field.Text',
alias: 'widget.tinymce',

// TinyMCE Settings specified for this instance of the editor.
tinymceSettings: null,

// Validation properties such as allowBlank Are all defined in super classes or Mixins

// HTML markup for this field
// The inner div will get an ID by {link #onRender} and will be hidden by tinyMCE rendering initialization.
// Ext4 Open question: should the markaup contain a hidden textarea as in Ext.form.field.HtmlEditor?
fieldSubTpl: [
'<div id="{id}" ',
'<tpl if="name">name="{name}" </tpl>',
'<tpl if="size">size="{size}" </tpl>',
'class="{fieldCls}">',
'<div></div>',
'</div>',
{
compiled: true,
disableFormats: true
}
],



/** ----------------------------------------------------------
*/
constructor: function(cfg) {

var config = {
tinymceSettings: {
accessibility_focus: false
}
};

Ext.apply(config, cfg);

// Add events
this.addEvents({
"editorcreated": true
});

this.callParent([config]); //Ext4
},

/** ----------------------------------------------------------
*/
initComponent: function() {
this.callParent(); // Ext4: added
this.tinymceSettings = this.tinymceSettings || {};
Ext.ux.TinyMCE.initTinyMCE({ language: this.tinymceSettings.language });
},

/* ----------------------------------------------------------
* Ext4 Open question: Is this necessary? originalValue will be set by form.Field.initValue
* /
initEvents: function() {
this.originalValue = this.getValue();
},*/

/** ----------------------------------------------------------
* Ext4: Rendering from Ext3 version removed, because the <div> wrapper is
* already provided by fieldSubTpl
*/
onRender: function(ct, position) {
this.callParent(arguments); // Ext4: added (makes this.inputEl available)

// Fix size if it was specified in config
if (Ext.type(this.width) == "number") {
this.tinymceSettings.width = this.width;
}
if (Ext.type(this.height) == "number") {
this.tinymceSettings.height = this.height;
}


var divForTinyEl = this.inputEl.first(),
id = Ext.id(divForTinyEl);
// Create TinyMCE editor within inputEl > div
this.ed = new tinymce.Editor(id, this.tinymceSettings);

// Validate value onKeyPress
var validateContentTask = new Ext.util.DelayedTask( this.validate, this );

this.ed.onKeyPress.add(Ext.bind(function(ed, controlManager) {
validateContentTask.delay( 250 );
}, this));

// Set up editor events' handlers
this.ed.onBeforeRenderUI.add(Ext.bind(function(ed, controlManager) {
// Replace control manager
ed.controlManager = new ControlManager(this, ed);
}, this));

this.ed.onPostRender.add(Ext.bind(function(ed, controlManager) {
var s = ed.settings,

// Modify markup
tbar = Ext.get(Ext.DomQuery.selectNode("#" + this.ed.id + "_tbl td.mceToolbar"));
if( tbar != null ) {
// If toolbar is present
var tbars = tbar.select("> table.mceToolbar");
Ext.core.DomHelper
.append( tbar,
{ tag: "div", id: this.ed.id + "_xtbar", style: { overflow: "hidden"} }
, true )
.appendChild(tbars);
}

// Change window manager
ed.windowManager = new WindowManager({
editor: this.ed,
manager: this.manager
});
// Patch css-style for validation body like ExtJS
Ext.get(ed.getContentAreaContainer()).addCls('patch-content-body'); // Ext4 code

// Event of focused body
Ext.Element.fly(s.content_editable ? ed.getBody() : ed.getWin())
.on("focus", this.onFocus, this);

// Event of blur body
Ext.Element.fly(s.content_editable ? ed.getBody() : ed.getWin())
.on("blur", this.onBlur, this,
this.inEditor && Ext.isWindows && Ext.isGecko ? { buffer: 10} : null
);

}, this));

// Set event handler on editor init.
//this.ed.onInit.add(Ext.bind(function() {
//}, this));

// Wire "change" event
this.ed.onChange.add(Ext.bind(function(ed, l) {
this.fireEvent("change", ed, l);
}, this));

// Render the editor
this.ed.render();
tinyMCE.add(this.ed);

// Fix editor size when control will be visible
// Ext4: Somehow the variable size does not contain the right value if this function is called
// directly during rendering. Therefore delayed with Ext.defer
Ext.defer(
(function fixEditorSize() {

// If element is not visible yet, wait.
if( !this.isVisible() ) {
arguments.callee.defer( 50, this );
return;
}

var size = this.getSize();
this.withEd( function() {
this._setEditorSize( size.width, size.height );

// Indicate that editor is created
this.fireEvent("editorcreated");
});
}), 50, this);

}, // end onRender

/** ----------------------------------------------------------
* Ext4: There is no wrapEl anymore, we can directly return
* inputEl, which is the <div> from fieldSubTpl
*/
getResizeEl: function() {
return this.inputEl;
},


/** ----------------------------------------------------------
*/
initValue: function() {

if (!this.rendered)
this.callParent();
else {
if (this.value !== undefined) {
this.setValue(this.value);
}
else {
// Ext4: Don't know how to handle this, there is no hidden textarea like in Ext3 implementation
throw "Ext.ux.TinyMCE.initValue: No initial value available";
/* This is the code from Ext3 implementation
var v = this.textareaEl.value;
if ( v )
this.setValue( v );*/
}
}
},

/** ----------------------------------------------------------
*/
beforeDestroy: function() {
if( this.ed ) tinyMCE.remove( this.ed );
if( this.inputEl ) Ext.destroy( this.inputEl ); //Ext4
this.callParent(); // Ext4
},

/** ----------------------------------------------------------
* Returns content if Tiny Editor.
* In Ext4, it's not necessary to implement getValue in case of
* string based content.
* @return {String} The TinyMCE content, including HTML tags
*/
getRawValue : function(){

if( !this.rendered || !this.ed.initialized )
return Ext.value( this.value, '' );

var v = this.ed.getContent();
if(v === this.emptyText){
v = '';
}
return v;
},

/** ----------------------------------------------------------
* In Ext4, it's not necessary to implement setValue in case of
* string based content.
*/
setRawValue: function(v) {
this.value = v;
if (this.rendered)
this.withEd(function() {
this.ed.undoManager.clear();
this.ed.setContent(v === null || v === undefined ? '' : v);
this.ed.startContent = this.ed.getContent({ format: 'raw' });
});
},

/**
* Since {@link #processRawValue} strips HTML tags from the Tiny text.
* this method is overwritten so that text with HTML tags will be submitted.
* @return {String} The value to be submitted, including HTML tags
*/
getSubmitValue: function() {
return this.getRawValue();
},

/** ----------------------------------------------------------
*/
isDirty: function() {
if (this.disabled || !this.rendered) {
return false;
}
return this.ed && this.ed.initialized && this.ed.isDirty();
},

/** ----------------------------------------------------------
*/
syncValue: function() {
if (this.rendered && this.ed.initialized)
this.ed.save();
},

/** ----------------------------------------------------------
*/
getEd: function() {
return this.ed;
},

/** ----------------------------------------------------------
*/
disable: function() {
this.withEd(function() {
var bodyEl = this.ed.getBody();
bodyEl = Ext.get(bodyEl);

if (bodyEl.hasCls('mceContentBody')) {
bodyEl.removeCls('mceContentBody');
bodyEl.addCls('mceNonEditable');
}
});
},

/** ----------------------------------------------------------
*/
enable: function() {
this.withEd(function() {
var bodyEl = this.ed.getBody();
bodyEl = Ext.get(bodyEl);

if (bodyEl.hasCls('mceNonEditable')) {
bodyEl.removeCls('mceNonEditable');
bodyEl.addCls('mceContentBody');
}
});
},

/** ----------------------------------------------------------
*/
onResize: function(aw, ah) {
if( Ext.type( aw ) != "number" ){
aw = this.getWidth();
}
if( Ext.type(ah) != "number" ){
ah = this.getHeight();
}
if (aw == 0 || ah == 0)
return;

if( this.rendered && this.isVisible() ) {
this.withEd(function() { this._setEditorSize( aw, ah ); });
}
},

/** ----------------------------------------------------------
Sets control size to the given width and height
*/
_setEditorSize: function( width, height ) {

// We currently support only advanced theme resize
if( !this.ed.theme.AdvancedTheme ) return;

// Minimal width and height for advanced theme
if( width < 100 ) width = 100;
if( height < 129 ) height = 129;

// Set toolbar div width
var edTable = Ext.get(this.ed.id + "_tbl"),
edIframe = Ext.get(this.ed.id + "_ifr"),
edToolbar = Ext.get(this.ed.id + "_xtbar"),
toolbarWidth = width,
toolbarHeight = 0;

if( edTable )
toolbarWidth = width - edTable.getFrameWidth( "lr" );

if( edToolbar ) {
toolbarHeight = edToolbar.getHeight();
var toolbarTd = edToolbar.findParent( "td", 5, true );
toolbarHeight += toolbarTd.getFrameWidth( "tb" );
edToolbar.setWidth( toolbarWidth );
}

/*
* Ext4: toolbar is a child of <tbody> which is a child of edTable
*var edStatusbarTd = edTable.child( ".mceStatusbar" );
*/
var edStatusbarTd = edTable.down( ".mceStatusbar" ),
statusbarHeight = 0;

if( edStatusbarTd ) {
statusbarHeight += edStatusbarTd.getHeight();
}

var iframeHeight = height - toolbarHeight - statusbarHeight,
iframeTd = edIframe.findParent( "td", 5, true );
if( iframeTd )
iframeHeight -= iframeTd.getFrameWidth( "tb" );

// Resize iframe and container
edTable.setSize( width, height );
edIframe.setSize( toolbarWidth, iframeHeight );
},

/** ----------------------------------------------------------
*/
focus: function(selectText, delay) {
if (delay) {
this.focus.defer(typeof delay == 'number' ? delay : 10, this, [selectText, false]);
return;
}

this.withEd(function() {
this.ed.focus();
/*if (selectText === true) {
// TODO: Select editor's content
}*/
});

return this;
},

/** ----------------------------------------------------------
* Ext4: renamed from processValue to processRawValue
* (overwrites Ext.form.field.Base.processRawValue)
* Strips HTML tags so that validation can be performed on the raw text
*/
processRawValue : function( value ){
return Ext.util.Format.stripTags( value );
},

/* ----------------------------------------------------------
* no need to implement validateValue as in Ext3 implementation,
* all functionality in Ext.form.field.Text.getErrors
*/

/** ----------------------------------------------------------
If ed (local editor instance) is already initialized, calls
specified function directly. Otherwise - adds it to ed.onInit event.
*/
withEd: function(func) {

// If editor is not created yet, reschedule this call.
if (!this.ed) this.on(
"editorcreated",
function() { this.withEd(func); },
this);

// Else if editor is created and initialized
else if (this.ed.initialized) func.call(this);

// Else if editor is created but not initialized yet.
else this.ed.onInit.add(Ext.bind(function() { Ext.defer(func, 10, this); }, this));
},

// Add static members, see documentation of Ext.Class
statics: {

/**
Static field with all the plugins that should be loaded by TinyMCE.
Should be set before first component would be created.
@static
*/
tinymcePlugins: "pagebreak,style,layer,table,advhr,advimage,advlink,emotions,iespell,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,noneditable,visualchars,nonbreaking,xhtmlxtras,template",

/** ----------------------------------------------------------
Inits TinyMCE and other necessary dependencies.
*/
initTinyMCE: function(settings) {
if (!tmceInitialized) {

// Create lazy classes
/** ----------------------------------------------------------
WindowManager
*/
WindowManager = Ext.extend( tinymce.WindowManager, {

/** ----------------------------------------------------------
Config parameters:
editor - reference to TinyMCE intstance.
mangager - WindowGroup to use for the popup window. Could be empty.
*/
constructor: function( cfg ) {
WindowManager.superclass.constructor.call(this, cfg.editor);

// Set window group
this.manager = cfg.manager;
},

/** ----------------------------------------------------------
*/
alert: function(txt, cb, s) {
Ext.MessageBox.alert("", txt, function() {
if (!Ext.isEmpty(cb)) {
cb.call(this);
}
}, s);
},

/** ----------------------------------------------------------
*/
confirm: function(txt, cb, s) {
Ext.MessageBox.confirm("", txt, function(btn) {
if (!Ext.isEmpty(cb)) {
cb.call(this, btn == "yes");
}
}, s);
},

/** ----------------------------------------------------------
*/
open: function(s, p) {

s = s || {};
p = p || {};

if (!s.type)
this.bookmark = this.editor.selection.getBookmark('simple');

s.width = parseInt(s.width || 320);
s.height = parseInt(s.height || 240) + (tinymce.isIE ? 8 : 0);
s.min_width = parseInt(s.min_width || 150);
s.min_height = parseInt(s.min_height || 100);
s.max_width = parseInt(s.max_width || 2000);
s.max_height = parseInt(s.max_height || 2000);
s.movable = true;
s.resizable = true;
p.mce_width = s.width;
p.mce_height = s.height;
p.mce_inline = true;

this.features = s;
this.params = p;

var win = new Ext.Window(
{
title: s.name,
width: s.width,
height: s.height,
minWidth: s.min_width,
minHeight: s.min_height,
resizable: true,
maximizable: s.maximizable,
minimizable: s.minimizable,
modal: true,
stateful: false,
constrain: true,
manager: this.manager,
layout: "fit",
items: [
new Ext.BoxComponent({
autoEl: {
tag: 'iframe',
src: s.url || s.file
},
style : 'border-width: 0px;'
})
]
});

p.mce_window_id = win.getId();

win.show(null,
function() {
if (s.left && s.top)
win.setPagePosition(s.left, s.top);
var pos = win.getPosition();
s.left = pos[0];
s.top = pos[1];
this.onOpen.dispatch(this, s, p);
},
this
);

return win;
},

/** ----------------------------------------------------------
*/
close: function(win) {

// Probably not inline
if (!win.tinyMCEPopup || !win.tinyMCEPopup.id) {
WindowManager.superclass.close.call(this, win);
return;
}

var w = Ext.getCmp(win.tinyMCEPopup.id);
if (w) {
this.onClose.dispatch(this);
w.close();
}
},

/** ----------------------------------------------------------
*/
setTitle: function(win, ti) {

// Probably not inline
if (!win.tinyMCEPopup || !win.tinyMCEPopup.id) {
WindowManager.superclass.setTitle.call(this, win, ti);
return;
}

var w = Ext.getCmp(win.tinyMCEPopup.id);
if (w) w.setTitle(ti);
},

/** ----------------------------------------------------------
*/
resizeBy: function(dw, dh, id) {

var w = Ext.getCmp(id);
if (w) {
var size = w.getSize();
w.setSize(size.width + dw, size.height + dh);
}
},

/** ----------------------------------------------------------
*/
focus: function(id) {
var w = Ext.getCmp(id);
if (w) w.setActive(true);
}

});

/** ----------------------------------------------------------
ControlManager
*/
ControlManager = Ext.extend( tinymce.ControlManager, {

// Reference to ExtJS control Ext.ux.TinyMCE.
control: null,

/** ----------------------------------------------------------
*/
constructor: function(control, ed, s) {
this.control = control;
ControlManager.superclass.constructor.call(this, ed, s);
},

/** ----------------------------------------------------------
*/
createDropMenu: function(id, s) {
// Call base method
var res = ControlManager.superclass.createDropMenu.call(this, id, s);

// Modify returned result
var orig = res.showMenu;
res.showMenu = function(x, y, px) {
orig.call(this, x, y, px);
Ext.fly('menu_' + this.id).setStyle("z-index", 200001);
};

return res;
},

/** ----------------------------------------------------------
*/
createColorSplitButton: function(id, s) {
// Call base method
var res = ControlManager.superclass.createColorSplitButton.call(this, id, s);

// Modify returned result
var orig = res.showMenu;
res.showMenu = function(x, y, px) {
orig.call(this, x, y, px);
Ext.fly(this.id + '_menu').setStyle("z-index", 200001);
};

return res;
}
});

// Init TinyMCE
var s = {
mode: "none",
plugins: Ext.ux.TinyMCE.tinymcePlugins,
theme: "advanced"
};
Ext.apply(s, settings);

if (!tinymce.dom.Event.domLoaded)
tinymce.dom.Event._pageInit();

tinyMCE.init(s);
tmceInitialized = true;
}
} // end method initTinyMCE
} // end statics
}); // Ext.define

})();


Sample form, taken from Ext4 form API


Ext.ux.TinyMCE.initTinyMCE();
Ext.onReady(function(){

Ext.create('Ext.form.Panel', {
title: 'Simple Form',
bodyPadding: 5,
//width: 350,

// The form will submit an AJAX request to this URL when submitted
url: 'save-form.php',

// Fields will be arranged vertically, stretched to full width
layout: 'anchor',
defaults: {
anchor: '100%'
},

// The fields
defaultType: 'textfield',
items: [{
fieldLabel: 'First Name',
name: 'first',
allowBlank: false
}, {
xtype: 'tinymce',
fieldLabel: 'Tiny',
name: 'tiny',
//width: 600,
height: 400,
allowBlank: false,
//minLength: 5, // validations from Ext.form.field.Text are available
tinymceSettings: {
theme: "advanced",
plugins: "pagebreak,style,layer,table,advhr,advimage,advlink,emotions,iespell,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,noneditable,visualchars,nonbreaking,xhtmlxtras,template",
theme_advanced_buttons1: "bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect",
theme_advanced_buttons2: "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
theme_advanced_buttons3: "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|",
theme_advanced_buttons4: "insertlayer,moveforward,movebackward,absolute,|,styleprops,|,cite,abbr,acronym,del,ins,attribs,|,visualchars,nonbreaking,template,pagebreak",
theme_advanced_toolbar_location: "top",
theme_advanced_toolbar_align: "left",
theme_advanced_statusbar_location: "bottom",
theme_advanced_resizing: false,
extended_valid_elements: "a[name|href|target|title|onclick],img[class|src|border=0|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size|noshade],font[face|size|color|style],span[class|align|style]",
template_external_list_url: "example_template_list.js"
}
}],

// Reset and Submit buttons
buttons: [{
text: 'Reset',
handler: function() {
this.up('form').getForm().reset();
}
}, {
text: 'Submit',
formBind: true, //only enabled once the form is valid
//disabled: true,
handler: function() {
var form = this.up('form').getForm();
if (form.isValid()) {
form.submit({
success: function(form, action) {
Ext.Msg.alert('Success', action.result.msg);
},
failure: function(form, action) {
Ext.Msg.alert('Failed', action.result.msg);
}
});
}
}
}],
renderTo: Ext.getBody()
});
});

googi
10 Aug 2011, 7:32 PM
hi ,I use your code with TinyMCE v3.4.4, when click the 'thml' button,
adpt throw a exception in this segment


manager: this.manager,
layout: "fit",
items: [
new Ext.BoxComponent({ // exception here


so I modify your adpt a little bit,hope it helps


var win = Ext.create('Ext.window.Window',{ title: s.name,
width: s.width,
height: s.height,
...
items: [
new Ext.Component({//Ext.BoxComponent
autoEl: {
tag: 'iframe',
src: s.url || s.file
},
style : 'border-width: 0px;'
})
]
});

dontbugme
16 Sep 2011, 9:55 AM
Just change Ext.BoxComponent to Ext.Component, and it will work.

lorezyra
9 Nov 2011, 6:31 PM
Any updates to this thread? I would like to know if anyone has made any further progress on this ux.

fridollin
2 Apr 2012, 8:04 AM
Hi,

I have a error in DOM with the following version:

Ext JS 4
TinyMCE 3.5
With IE9:
Document mode IE8 works fine :-)b
ut not with mode IE9, the editor text doesn't appear and I can't edit the text.

I have the following error with I try to changed the font.SCRIPT5022: DOM Exception: HIERARCHY_REQUEST_ERR (3)
ny_mce_src.js, line 16547 character 7 (http://www.sencha.com/forum/0)
...
rng.insertNode(caretContainer);

Do you have an idea?
Thanks for your help.

Best regards,
fridollin