1 //<script type="text/javascript">
5 * Copyright(c) 2006-2007, Ext JS, LLC.
8 * http://www.extjs.com/license
14 * Default CSS appears to render it as fixed text by default (should really be Sans-Serif)
15 * - IE ? - no idea how much works there.
23 * @class Ext.form.HtmlEditor
24 * @extends Ext.form.Field
25 * Provides a lightweight HTML Editor component.
26 * WARNING - THIS CURRENTlY ONLY WORKS ON FIREFOX - USE FCKeditor for a cross platform version
28 * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
29 * supported by this editor.</b><br/><br/>
30 * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within
31 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
33 Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
35 * @cfg {Array} toolbars Array of toolbars. - defaults to just the Standard one
39 * @cfg {String} createLinkText The default text for the create link prompt
41 createLinkText : 'Please enter the URL for the link:',
43 * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /)
45 defaultLinkValue : 'http:/'+'/',
48 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
53 * @cfg {Number} height (in pixels)
57 * @cfg {Number} width (in pixels)
64 validationEvent : false,
68 sourceEditMode : false,
69 onFocus : Roo.emptyFn,
73 defaultAutoCreate : { // modified by initCompnoent..
75 style:"width:500px;height:300px;",
80 initComponent : function(){
84 * Fires when the editor is fully initialized (including the iframe)
85 * @param {HtmlEditor} this
90 * Fires when the editor is first receives the focus. Any insertion must wait
91 * until after this event.
92 * @param {HtmlEditor} this
97 * Fires before the textarea is updated with content from the editor iframe. Return false
99 * @param {HtmlEditor} this
100 * @param {String} html
105 * Fires before the iframe editor is updated with content from the textarea. Return false
106 * to cancel the push.
107 * @param {HtmlEditor} this
108 * @param {String} html
113 * Fires when the textarea is updated with content from the editor iframe.
114 * @param {HtmlEditor} this
115 * @param {String} html
120 * Fires when the iframe editor is updated with content from the textarea.
121 * @param {HtmlEditor} this
122 * @param {String} html
126 * @event editmodechange
127 * Fires when the editor switches edit modes
128 * @param {HtmlEditor} this
129 * @param {Boolean} sourceEdit True if source edit, false if standard editing.
131 editmodechange: true,
134 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
135 * @param {HtmlEditor} this
139 this.defaultAutoCreate = {
141 style:'width: ' + this.width + 'px;height: ' + this.height + 'px;',
147 * Protected method that will not generally be called directly. It
148 * is called when the editor creates its toolbar. Override this method if you need to
149 * add custom toolbar buttons.
150 * @param {HtmlEditor} editor
152 createToolbar : function(editor){
153 if (!editor.toolbars || !editor.toolbars.length) {
154 editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
157 for (var i =0 ; i < editor.toolbars.length;i++) {
158 editor.toolbars[i] = Roo.factory(editor.toolbars[i], Roo.form.HtmlEditor);
159 editor.toolbars[i].init(editor);
166 * Protected method that will not generally be called directly. It
167 * is called when the editor initializes the iframe with HTML contents. Override this method if you
168 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
170 getDocMarkup : function(){
171 return '<html><head><style type="text/css">body{border:0;margin:0;padding:3px;height:98%;cursor:text;}</style></head><body></body></html>';
175 onRender : function(ct, position)
178 Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
179 this.el.dom.style.border = '0 none';
180 this.el.dom.setAttribute('tabIndex', -1);
181 this.el.addClass('x-hidden');
182 if(Roo.isIE){ // fix IE 1px bogus margin
183 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
185 this.wrap = this.el.wrap({
186 cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
189 if (this.resizable) {
190 this.resizeEl = new Roo.Resizable(this.wrap, {
194 minHeight : this.height,
196 handles : this.resizable,
199 resize : function(r, w, h) {
200 _t.onResize(w,h); // -something
207 this.frameId = Roo.id();
209 this.createToolbar(this);
213 var iframe = this.wrap.createChild({
218 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
222 // console.log(iframe);
223 //this.wrap.dom.appendChild(iframe);
225 this.iframe = iframe.dom;
229 this.doc.designMode = 'on';
232 this.doc.write(this.getDocMarkup());
236 var task = { // must defer to wait for browser to be ready
238 //console.log("run task?" + this.doc.readyState);
240 if(this.doc.body || this.doc.readyState == 'complete'){
242 this.doc.designMode="on";
246 Roo.TaskMgr.stop(task);
247 this.initEditor.defer(10, this);
254 Roo.TaskMgr.start(task);
257 this.setSize(this.wrap.getSize());
260 this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
261 // should trigger onReize..
266 onResize : function(w, h)
268 //Roo.log('resize: ' +w + ',' + h );
269 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
270 if(this.el && this.iframe){
271 if(typeof w == 'number'){
272 var aw = w - this.wrap.getFrameWidth('lr');
273 this.el.setWidth(this.adjustWidth('textarea', aw));
274 this.iframe.style.width = aw + 'px';
276 if(typeof h == 'number'){
278 for (var i =0; i < this.toolbars.length;i++) {
279 // fixme - ask toolbars for heights?
280 tbh += this.toolbars[i].tb.el.getHeight();
281 if (this.toolbars[i].footer) {
282 tbh += this.toolbars[i].footer.el.getHeight();
289 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
290 ah -= 10; // knock a few pixes off for look..
291 this.el.setHeight(this.adjustWidth('textarea', ah));
292 this.iframe.style.height = ah + 'px';
294 (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
301 * Toggles the editor between standard and source edit mode.
302 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
304 toggleSourceEdit : function(sourceEditMode){
306 this.sourceEditMode = sourceEditMode === true;
308 if(this.sourceEditMode){
311 this.iframe.className = 'x-hidden';
312 this.el.removeClass('x-hidden');
313 this.el.dom.removeAttribute('tabIndex');
318 this.iframe.className = '';
319 this.el.addClass('x-hidden');
320 this.el.dom.setAttribute('tabIndex', -1);
323 this.setSize(this.wrap.getSize());
324 this.fireEvent('editmodechange', this, this.sourceEditMode);
327 // private used internally
328 createLink : function(){
329 var url = prompt(this.createLinkText, this.defaultLinkValue);
330 if(url && url != 'http:/'+'/'){
331 this.relayCmd('createlink', url);
335 // private (for BoxComponent)
336 adjustSize : Roo.BoxComponent.prototype.adjustSize,
338 // private (for BoxComponent)
339 getResizeEl : function(){
343 // private (for BoxComponent)
344 getPositionEl : function(){
349 initEvents : function(){
350 this.originalValue = this.getValue();
354 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
357 markInvalid : Roo.emptyFn,
359 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
362 clearInvalid : Roo.emptyFn,
364 setValue : function(v){
365 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
370 * Protected method that will not generally be called directly. If you need/want
371 * custom HTML cleanup, this is the method you should override.
372 * @param {String} html The HTML to be cleaned
373 * return {String} The cleaned HTML
375 cleanHtml : function(html){
378 if(Roo.isSafari){ // strip safari nonsense
379 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
382 if(html == ' '){
389 * Protected method that will not generally be called directly. Syncs the contents
390 * of the editor iframe with the textarea.
392 syncValue : function(){
393 if(this.initialized){
394 var bd = (this.doc.body || this.doc.documentElement);
396 var html = bd.innerHTML;
398 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
399 var m = bs.match(/text-align:(.*?);/i);
401 html = '<div style="'+m[0]+'">' + html + '</div>';
404 html = this.cleanHtml(html);
405 if(this.fireEvent('beforesync', this, html) !== false){
406 this.el.dom.value = html;
407 this.fireEvent('sync', this, html);
413 * Protected method that will not generally be called directly. Pushes the value of the textarea
414 * into the iframe editor.
416 pushValue : function(){
417 if(this.initialized){
418 var v = this.el.dom.value;
423 if(this.fireEvent('beforepush', this, v) !== false){
424 var d = (this.doc.body || this.doc.documentElement);
427 this.el.dom.value = d.innerHTML;
428 this.fireEvent('push', this, v);
434 deferFocus : function(){
435 this.focus.defer(10, this);
440 if(this.win && !this.sourceEditMode){
447 assignDocWin: function()
449 var iframe = this.iframe;
452 this.doc = iframe.contentWindow.document;
453 this.win = iframe.contentWindow;
455 if (!Roo.get(this.frameId)) {
458 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
459 this.win = Roo.get(this.frameId).dom.contentWindow;
464 initEditor : function(){
465 //console.log("INIT EDITOR");
470 this.doc.designMode="on";
472 this.doc.write(this.getDocMarkup());
475 var dbody = (this.doc.body || this.doc.documentElement);
476 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
477 // this copies styles from the containing element into thsi one..
478 // not sure why we need all of this..
479 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
480 ss['background-attachment'] = 'fixed'; // w3c
481 dbody.bgProperties = 'fixed'; // ie
482 Roo.DomHelper.applyStyles(dbody, ss);
483 Roo.EventManager.on(this.doc, {
484 'mousedown': this.onEditorEvent,
485 'dblclick': this.onEditorEvent,
486 'click': this.onEditorEvent,
487 'keyup': this.onEditorEvent,
492 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
494 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
495 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
497 this.initialized = true;
499 this.fireEvent('initialize', this);
504 onDestroy : function(){
510 for (var i =0; i < this.toolbars.length;i++) {
511 // fixme - ask toolbars for heights?
512 this.toolbars[i].onDestroy();
515 this.wrap.dom.innerHTML = '';
521 onFirstFocus : function(){
526 this.activated = true;
527 for (var i =0; i < this.toolbars.length;i++) {
528 this.toolbars[i].onFirstFocus();
531 if(Roo.isGecko){ // prevent silly gecko errors
533 var s = this.win.getSelection();
534 if(!s.focusNode || s.focusNode.nodeType != 3){
535 var r = s.getRangeAt(0);
536 r.selectNodeContents((this.doc.body || this.doc.documentElement));
541 this.execCmd('useCSS', true);
542 this.execCmd('styleWithCSS', false);
545 this.fireEvent('activate', this);
549 adjustFont: function(btn){
550 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
551 //if(Roo.isSafari){ // safari
554 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
555 if(Roo.isSafari){ // safari
556 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
557 v = (v < 10) ? 10 : v;
558 v = (v > 48) ? 48 : v;
559 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
564 v = Math.max(1, v+adjust);
566 this.execCmd('FontSize', v );
569 onEditorEvent : function(e){
570 this.fireEvent('editorevent', this, e);
571 // this.updateToolbar();
575 insertTag : function(tg)
577 // could be a bit smarter... -> wrap the current selected tRoo..
579 this.execCmd("formatblock", tg);
583 insertText : function(txt)
587 range = this.createRange();
588 range.deleteContents();
589 //alert(Sender.getAttribute('label'));
591 range.insertNode(this.doc.createTextNode(txt));
595 relayBtnCmd : function(btn){
596 this.relayCmd(btn.cmd);
600 * Executes a Midas editor command on the editor document and performs necessary focus and
601 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
602 * @param {String} cmd The Midas command
603 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
605 relayCmd : function(cmd, value){
607 this.execCmd(cmd, value);
608 this.fireEvent('editorevent', this);
609 //this.updateToolbar();
614 * Executes a Midas editor command directly on the editor document.
615 * For visual commands, you should use {@link #relayCmd} instead.
616 * <b>This should only be called after the editor is initialized.</b>
617 * @param {String} cmd The Midas command
618 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
620 execCmd : function(cmd, value){
621 this.doc.execCommand(cmd, false, value === undefined ? null : value);
627 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
629 * @param {String} text
631 insertAtCursor : function(text){
637 var r = this.doc.selection.createRange();
644 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
646 this.execCmd('InsertHTML', text);
651 mozKeyPress : function(e){
653 var c = e.getCharCode(), cmd;
656 c = String.fromCharCode(c).toLowerCase();
667 this.cleanUpPaste.defer(100, this);
683 fixKeys : function(){ // load time branching for fastest keydown performance
686 var k = e.getKey(), r;
689 r = this.doc.selection.createRange();
692 r.pasteHTML('    ');
699 r = this.doc.selection.createRange();
701 var target = r.parentElement();
702 if(!target || target.tagName.toLowerCase() != 'li'){
704 r.pasteHTML('<br />');
710 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
711 this.cleanUpPaste.defer(100, this);
717 }else if(Roo.isOpera){
723 this.execCmd('InsertHTML','    ');
726 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
727 this.cleanUpPaste.defer(100, this);
732 }else if(Roo.isSafari){
738 this.execCmd('InsertText','\t');
742 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
743 this.cleanUpPaste.defer(100, this);
751 getAllAncestors: function()
753 var p = this.getSelectedNode();
756 a.push(p); // push blank onto stack..
757 p = this.getParentElement();
761 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
765 a.push(this.doc.body);
772 getSelection : function()
775 return Roo.isIE ? this.doc.selection : this.win.getSelection();
778 getSelectedNode: function()
780 // this may only work on Gecko!!!
782 // should we cache this!!!!
787 var range = this.createRange(this.getSelection());
790 var parent = range.parentElement();
792 var testRange = range.duplicate();
793 testRange.moveToElementText(parent);
794 if (testRange.inRange(range)) {
797 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
800 parent = parent.parentElement;
806 var ar = range.endContainer.childNodes;
808 ar = range.commonAncestorContainer.childNodes;
812 var other_nodes = [];
813 var has_other_nodes = false;
814 for (var i=0;i<ar.length;i++) {
815 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
818 // fullly contained node.
820 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
825 // probably selected..
826 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
827 other_nodes.push(ar[i]);
830 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
835 has_other_nodes = true;
837 if (!nodes.length && other_nodes.length) {
840 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
846 createRange: function(sel)
848 // this has strange effects when using with
849 // top toolbar - not sure if it's a great idea.
850 //this.editor.contentWindow.focus();
851 if (typeof sel != "undefined") {
853 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
855 return this.doc.createRange();
858 return this.doc.createRange();
861 getParentElement: function()
865 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
867 var range = this.createRange(sel);
870 var p = range.commonAncestorContainer;
871 while (p.nodeType == 3) { // text node
883 // BC Hacks - cause I cant work out what i was trying to do..
884 rangeIntersectsNode : function(range, node)
886 var nodeRange = node.ownerDocument.createRange();
888 nodeRange.selectNode(node);
891 nodeRange.selectNodeContents(node);
894 return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) == -1 &&
895 range.compareBoundaryPoints(Range.START_TO_END, nodeRange) == 1;
897 rangeCompareNode : function(range, node) {
898 var nodeRange = node.ownerDocument.createRange();
900 nodeRange.selectNode(node);
902 nodeRange.selectNodeContents(node);
904 var nodeIsBefore = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) == 1;
905 var nodeIsAfter = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) == -1;
907 if (nodeIsBefore && !nodeIsAfter)
909 if (!nodeIsBefore && nodeIsAfter)
911 if (nodeIsBefore && nodeIsAfter)
917 // private? - in a new class?
918 cleanUpPaste : function()
920 // cleans up the whole document..
921 // console.log('cleanuppaste');
922 this.cleanUpChildren(this.doc.body);
926 cleanUpChildren : function (n)
928 if (!n.childNodes.length) {
931 for (var i = n.childNodes.length-1; i > -1 ; i--) {
932 this.cleanUpChild(n.childNodes[i]);
939 cleanUpChild : function (node)
942 if (node.nodeName == "#text") {
943 // clean up silly Windows -- stuff?
946 if (node.nodeName == "#comment") {
947 node.parentNode.removeChild(node);
948 // clean up silly Windows -- stuff?
952 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
954 node.parentNode.removeChild(node);
958 if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
959 this.cleanUpChildren(node);
960 // inserts everything just before this node...
961 while (node.childNodes.length) {
962 var cn = node.childNodes[0];
963 node.removeChild(cn);
964 node.parentNode.insertBefore(cn, node);
966 node.parentNode.removeChild(node);
970 if (!node.attributes || !node.attributes.length) {
971 this.cleanUpChildren(node);
975 function cleanAttr(n,v)
978 if (v.match(/^\./) || v.match(/^\//)) {
981 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
984 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
985 node.removeAttribute(n);
989 function cleanStyle(n,v)
991 if (v.match(/expression/)) { //XSS?? should we even bother..
992 node.removeAttribute(n);
997 var parts = v.split(/;/);
998 Roo.each(parts, function(p) {
999 p = p.replace(/\s+/g,'');
1003 var l = p.split(':').shift().replace(/\s+/g,'');
1005 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1006 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1007 node.removeAttribute(n);
1017 for (var i = node.attributes.length-1; i > -1 ; i--) {
1018 var a = node.attributes[i];
1020 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1021 node.removeAttribute(a.name);
1024 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1025 cleanAttr(a.name,a.value); // fixme..
1028 if (a.name == 'style') {
1029 cleanStyle(a.name,a.value);
1031 /// clean up MS crap..
1032 if (a.name == 'class') {
1033 if (a.value.match(/^Mso/)) {
1034 node.className = '';
1044 this.cleanUpChildren(node);
1050 // hide stuff that is not compatible
1068 * @cfg {String} fieldClass @hide
1071 * @cfg {String} focusClass @hide
1074 * @cfg {String} autoCreate @hide
1077 * @cfg {String} inputType @hide
1080 * @cfg {String} invalidClass @hide
1083 * @cfg {String} invalidText @hide
1086 * @cfg {String} msgFx @hide
1089 * @cfg {String} validateOnBlur @hide
1093 Roo.form.HtmlEditor.white = [
1094 'area', 'br', 'img', 'input', 'hr', 'wbr',
1096 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1097 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1098 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1099 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1100 'table', 'ul', 'xmp',
1102 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1105 'dir', 'menu', 'ol', 'ul', 'dl',
1111 Roo.form.HtmlEditor.black = [
1112 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1114 'base', 'basefont', 'bgsound', 'blink', 'body',
1115 'frame', 'frameset', 'head', 'html', 'ilayer',
1116 'iframe', 'layer', 'link', 'meta', 'object',
1117 'script', 'style' ,'title', 'xml' // clean later..
1119 Roo.form.HtmlEditor.clean = [
1120 'script', 'style', 'title', 'xml'
1122 Roo.form.HtmlEditor.remove = [
1127 Roo.form.HtmlEditor.ablack = [
1131 Roo.form.HtmlEditor.aclean = [
1132 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1136 Roo.form.HtmlEditor.pwhite= [
1137 'http', 'https', 'mailto'
1140 Roo.form.HtmlEditor.cwhite= [