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.
27 * This has been tested on Fireforx / Chrome.. IE may not be so great..
29 * <br><br><b>Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
30 * supported by this editor.</b><br/><br/>
31 * An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within
32 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
34 Roo.form.HtmlEditor = Roo.extend(Roo.form.Field, {
36 * @cfg {Array} toolbars Array of toolbars. - defaults to just the Standard one
40 * @cfg {String} createLinkText The default text for the create link prompt
42 createLinkText : 'Please enter the URL for the link:',
44 * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /)
46 defaultLinkValue : 'http:/'+'/',
49 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
54 * @cfg {Number} height (in pixels)
58 * @cfg {Number} width (in pixels)
63 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
72 validationEvent : false,
76 sourceEditMode : false,
77 onFocus : Roo.emptyFn,
81 defaultAutoCreate : { // modified by initCompnoent..
83 style:"width:500px;height:300px;",
88 initComponent : function(){
92 * Fires when the editor is fully initialized (including the iframe)
93 * @param {HtmlEditor} this
98 * Fires when the editor is first receives the focus. Any insertion must wait
99 * until after this event.
100 * @param {HtmlEditor} this
105 * Fires before the textarea is updated with content from the editor iframe. Return false
106 * to cancel the sync.
107 * @param {HtmlEditor} this
108 * @param {String} html
113 * Fires before the iframe editor is updated with content from the textarea. Return false
114 * to cancel the push.
115 * @param {HtmlEditor} this
116 * @param {String} html
121 * Fires when the textarea is updated with content from the editor iframe.
122 * @param {HtmlEditor} this
123 * @param {String} html
128 * Fires when the iframe editor is updated with content from the textarea.
129 * @param {HtmlEditor} this
130 * @param {String} html
134 * @event editmodechange
135 * Fires when the editor switches edit modes
136 * @param {HtmlEditor} this
137 * @param {Boolean} sourceEdit True if source edit, false if standard editing.
139 editmodechange: true,
142 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
143 * @param {HtmlEditor} this
147 this.defaultAutoCreate = {
149 style:'width: ' + this.width + 'px;height: ' + this.height + 'px;',
155 * Protected method that will not generally be called directly. It
156 * is called when the editor creates its toolbar. Override this method if you need to
157 * add custom toolbar buttons.
158 * @param {HtmlEditor} editor
160 createToolbar : function(editor){
161 if (!editor.toolbars || !editor.toolbars.length) {
162 editor.toolbars = [ new Roo.form.HtmlEditor.ToolbarStandard() ]; // can be empty?
165 for (var i =0 ; i < editor.toolbars.length;i++) {
166 editor.toolbars[i] = Roo.factory(
167 typeof(editor.toolbars[i]) == 'string' ?
168 { xtype: editor.toolbars[i]} : editor.toolbars[i],
169 Roo.form.HtmlEditor);
170 editor.toolbars[i].init(editor);
177 * Protected method that will not generally be called directly. It
178 * is called when the editor initializes the iframe with HTML contents. Override this method if you
179 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
181 getDocMarkup : function(){
184 if (this.stylesheets === false) {
186 Roo.get(document.head).select('style').each(function(node) {
187 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
190 Roo.get(document.head).select('link').each(function(node) {
191 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
194 } else if (!this.stylesheets.length) {
196 st = '<style type="text/css">' +
197 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
200 Roo.each(this.stylesheets, function(s) {
201 st += '<link rel="stylesheet" type="text/css" href="' + s +'" />'
206 st += '<style type="text/css">' +
207 'IMG { cursor: pointer } ' +
211 return '<html><head>' + st +
212 //<style type="text/css">' +
213 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
215 ' </head><body class="roo-htmleditor-body"></body></html>';
219 onRender : function(ct, position)
222 Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
223 this.el.dom.style.border = '0 none';
224 this.el.dom.setAttribute('tabIndex', -1);
225 this.el.addClass('x-hidden');
226 if(Roo.isIE){ // fix IE 1px bogus margin
227 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
229 this.wrap = this.el.wrap({
230 cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
233 if (this.resizable) {
234 this.resizeEl = new Roo.Resizable(this.wrap, {
238 minHeight : this.height,
240 handles : this.resizable,
243 resize : function(r, w, h) {
244 _t.onResize(w,h); // -something
251 this.frameId = Roo.id();
253 this.createToolbar(this);
257 var iframe = this.wrap.createChild({
262 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
266 // console.log(iframe);
267 //this.wrap.dom.appendChild(iframe);
269 this.iframe = iframe.dom;
273 this.doc.designMode = 'on';
276 this.doc.write(this.getDocMarkup());
280 var task = { // must defer to wait for browser to be ready
282 //console.log("run task?" + this.doc.readyState);
284 if(this.doc.body || this.doc.readyState == 'complete'){
286 this.doc.designMode="on";
290 Roo.TaskMgr.stop(task);
291 this.initEditor.defer(10, this);
298 Roo.TaskMgr.start(task);
301 this.setSize(this.wrap.getSize());
304 this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
305 // should trigger onReize..
310 onResize : function(w, h)
312 //Roo.log('resize: ' +w + ',' + h );
313 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
314 if(this.el && this.iframe){
315 if(typeof w == 'number'){
316 var aw = w - this.wrap.getFrameWidth('lr');
317 this.el.setWidth(this.adjustWidth('textarea', aw));
318 this.iframe.style.width = aw + 'px';
320 if(typeof h == 'number'){
322 for (var i =0; i < this.toolbars.length;i++) {
323 // fixme - ask toolbars for heights?
324 tbh += this.toolbars[i].tb.el.getHeight();
325 if (this.toolbars[i].footer) {
326 tbh += this.toolbars[i].footer.el.getHeight();
333 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
334 ah -= 5; // knock a few pixes off for look..
335 this.el.setHeight(this.adjustWidth('textarea', ah));
336 this.iframe.style.height = ah + 'px';
338 (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
345 * Toggles the editor between standard and source edit mode.
346 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
348 toggleSourceEdit : function(sourceEditMode){
350 this.sourceEditMode = sourceEditMode === true;
352 if(this.sourceEditMode){
354 // Roo.log(this.syncValue());
356 this.iframe.className = 'x-hidden';
357 this.el.removeClass('x-hidden');
358 this.el.dom.removeAttribute('tabIndex');
362 // Roo.log(this.pushValue());
364 this.iframe.className = '';
365 this.el.addClass('x-hidden');
366 this.el.dom.setAttribute('tabIndex', -1);
369 this.setSize(this.wrap.getSize());
370 this.fireEvent('editmodechange', this, this.sourceEditMode);
373 // private used internally
374 createLink : function(){
375 var url = prompt(this.createLinkText, this.defaultLinkValue);
376 if(url && url != 'http:/'+'/'){
377 this.relayCmd('createlink', url);
381 // private (for BoxComponent)
382 adjustSize : Roo.BoxComponent.prototype.adjustSize,
384 // private (for BoxComponent)
385 getResizeEl : function(){
389 // private (for BoxComponent)
390 getPositionEl : function(){
395 initEvents : function(){
396 this.originalValue = this.getValue();
400 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
403 markInvalid : Roo.emptyFn,
405 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
408 clearInvalid : Roo.emptyFn,
410 setValue : function(v){
411 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
416 * Protected method that will not generally be called directly. If you need/want
417 * custom HTML cleanup, this is the method you should override.
418 * @param {String} html The HTML to be cleaned
419 * return {String} The cleaned HTML
421 cleanHtml : function(html){
424 if(Roo.isSafari){ // strip safari nonsense
425 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
428 if(html == ' '){
435 * Protected method that will not generally be called directly. Syncs the contents
436 * of the editor iframe with the textarea.
438 syncValue : function(){
439 if(this.initialized){
440 var bd = (this.doc.body || this.doc.documentElement);
441 //this.cleanUpPaste(); -- this is done else where and causes havoc..
442 var html = bd.innerHTML;
444 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
445 var m = bs.match(/text-align:(.*?);/i);
447 html = '<div style="'+m[0]+'">' + html + '</div>';
450 html = this.cleanHtml(html);
451 // fix up the special chars.. normaly like back quotes in word...
452 // however we do not want to do this with chinese..
453 html = html.replace(/([\x80-\uffff])/g, function (a, b) {
454 var cc = b.charCodeAt();
456 (cc >= 0x4E00 && cc < 0xA000 ) ||
457 (cc >= 0x3400 && cc < 0x4E00 ) ||
458 (cc >= 0xf900 && cc < 0xfb00 )
464 if(this.fireEvent('beforesync', this, html) !== false){
465 this.el.dom.value = html;
466 this.fireEvent('sync', this, html);
472 * Protected method that will not generally be called directly. Pushes the value of the textarea
473 * into the iframe editor.
475 pushValue : function(){
476 if(this.initialized){
477 var v = this.el.dom.value;
483 if(this.fireEvent('beforepush', this, v) !== false){
484 var d = (this.doc.body || this.doc.documentElement);
487 this.el.dom.value = d.innerHTML;
488 this.fireEvent('push', this, v);
494 deferFocus : function(){
495 this.focus.defer(10, this);
500 if(this.win && !this.sourceEditMode){
507 assignDocWin: function()
509 var iframe = this.iframe;
512 this.doc = iframe.contentWindow.document;
513 this.win = iframe.contentWindow;
515 if (!Roo.get(this.frameId)) {
518 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
519 this.win = Roo.get(this.frameId).dom.contentWindow;
524 initEditor : function(){
525 //console.log("INIT EDITOR");
530 this.doc.designMode="on";
532 this.doc.write(this.getDocMarkup());
535 var dbody = (this.doc.body || this.doc.documentElement);
536 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
537 // this copies styles from the containing element into thsi one..
538 // not sure why we need all of this..
539 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
540 ss['background-attachment'] = 'fixed'; // w3c
541 dbody.bgProperties = 'fixed'; // ie
542 Roo.DomHelper.applyStyles(dbody, ss);
543 Roo.EventManager.on(this.doc, {
544 //'mousedown': this.onEditorEvent,
545 'mouseup': this.onEditorEvent,
546 'dblclick': this.onEditorEvent,
547 'click': this.onEditorEvent,
548 'keyup': this.onEditorEvent,
553 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
555 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
556 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
558 this.initialized = true;
560 this.fireEvent('initialize', this);
565 onDestroy : function(){
571 for (var i =0; i < this.toolbars.length;i++) {
572 // fixme - ask toolbars for heights?
573 this.toolbars[i].onDestroy();
576 this.wrap.dom.innerHTML = '';
582 onFirstFocus : function(){
587 this.activated = true;
588 for (var i =0; i < this.toolbars.length;i++) {
589 this.toolbars[i].onFirstFocus();
592 if(Roo.isGecko){ // prevent silly gecko errors
594 var s = this.win.getSelection();
595 if(!s.focusNode || s.focusNode.nodeType != 3){
596 var r = s.getRangeAt(0);
597 r.selectNodeContents((this.doc.body || this.doc.documentElement));
602 this.execCmd('useCSS', true);
603 this.execCmd('styleWithCSS', false);
606 this.fireEvent('activate', this);
610 adjustFont: function(btn){
611 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
612 //if(Roo.isSafari){ // safari
615 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
616 if(Roo.isSafari){ // safari
617 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
618 v = (v < 10) ? 10 : v;
619 v = (v > 48) ? 48 : v;
620 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
625 v = Math.max(1, v+adjust);
627 this.execCmd('FontSize', v );
630 onEditorEvent : function(e){
631 this.fireEvent('editorevent', this, e);
632 // this.updateToolbar();
633 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
636 insertTag : function(tg)
638 // could be a bit smarter... -> wrap the current selected tRoo..
639 if (tg.toLowerCase() == 'span') {
641 range = this.createRange(this.getSelection());
642 var wrappingNode = this.doc.createElement("span");
643 wrappingNode.appendChild(range.extractContents());
644 range.insertNode(wrappingNode);
651 this.execCmd("formatblock", tg);
655 insertText : function(txt)
659 var range = this.createRange();
660 range.deleteContents();
661 //alert(Sender.getAttribute('label'));
663 range.insertNode(this.doc.createTextNode(txt));
667 relayBtnCmd : function(btn){
668 this.relayCmd(btn.cmd);
672 * Executes a Midas editor command on the editor document and performs necessary focus and
673 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
674 * @param {String} cmd The Midas command
675 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
677 relayCmd : function(cmd, value){
679 this.execCmd(cmd, value);
680 this.fireEvent('editorevent', this);
681 //this.updateToolbar();
686 * Executes a Midas editor command directly on the editor document.
687 * For visual commands, you should use {@link #relayCmd} instead.
688 * <b>This should only be called after the editor is initialized.</b>
689 * @param {String} cmd The Midas command
690 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
692 execCmd : function(cmd, value){
693 this.doc.execCommand(cmd, false, value === undefined ? null : value);
700 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
702 * @param {String} text | dom node..
704 insertAtCursor : function(text)
715 var r = this.doc.selection.createRange();
726 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
730 // from jquery ui (MIT licenced)
734 if (win.getSelection && win.getSelection().getRangeAt) {
735 range = win.getSelection().getRangeAt(0);
736 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
737 range.insertNode(node);
738 } else if (win.document.selection && win.document.selection.createRange) {
739 // no firefox support
740 var txt = typeof(text) == 'string' ? text : text.outerHTML;
741 win.document.selection.createRange().pasteHTML(txt);
743 // no firefox support
744 var txt = typeof(text) == 'string' ? text : text.outerHTML;
745 this.execCmd('InsertHTML', txt);
754 mozKeyPress : function(e){
756 var c = e.getCharCode(), cmd;
759 c = String.fromCharCode(c).toLowerCase();
773 this.cleanUpPaste.defer(100, this);
789 fixKeys : function(){ // load time branching for fastest keydown performance
792 var k = e.getKey(), r;
795 r = this.doc.selection.createRange();
798 r.pasteHTML('    ');
805 r = this.doc.selection.createRange();
807 var target = r.parentElement();
808 if(!target || target.tagName.toLowerCase() != 'li'){
810 r.pasteHTML('<br />');
816 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
817 this.cleanUpPaste.defer(100, this);
823 }else if(Roo.isOpera){
829 this.execCmd('InsertHTML','    ');
832 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
833 this.cleanUpPaste.defer(100, this);
838 }else if(Roo.isSafari){
844 this.execCmd('InsertText','\t');
848 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
849 this.cleanUpPaste.defer(100, this);
857 getAllAncestors: function()
859 var p = this.getSelectedNode();
862 a.push(p); // push blank onto stack..
863 p = this.getParentElement();
867 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
871 a.push(this.doc.body);
878 getSelection : function()
881 return Roo.isIE ? this.doc.selection : this.win.getSelection();
884 getSelectedNode: function()
886 // this may only work on Gecko!!!
888 // should we cache this!!!!
893 var range = this.createRange(this.getSelection()).cloneRange();
896 var parent = range.parentElement();
898 var testRange = range.duplicate();
899 testRange.moveToElementText(parent);
900 if (testRange.inRange(range)) {
903 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
906 parent = parent.parentElement;
911 // is ancestor a text element.
912 var ac = range.commonAncestorContainer;
913 if (ac.nodeType == 3) {
917 var ar = ac.childNodes;
920 var other_nodes = [];
921 var has_other_nodes = false;
922 for (var i=0;i<ar.length;i++) {
923 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
926 // fullly contained node.
928 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
933 // probably selected..
934 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
935 other_nodes.push(ar[i]);
939 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
944 has_other_nodes = true;
946 if (!nodes.length && other_nodes.length) {
949 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
955 createRange: function(sel)
957 // this has strange effects when using with
958 // top toolbar - not sure if it's a great idea.
959 //this.editor.contentWindow.focus();
960 if (typeof sel != "undefined") {
962 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
964 return this.doc.createRange();
967 return this.doc.createRange();
970 getParentElement: function()
974 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
976 var range = this.createRange(sel);
979 var p = range.commonAncestorContainer;
980 while (p.nodeType == 3) { // text node
991 * Range intersection.. the hard stuff...
995 * [ -- selected range --- ]
999 * if end is before start or hits it. fail.
1000 * if start is after end or hits it fail.
1002 * if either hits (but other is outside. - then it's not
1008 // @see http://www.thismuchiknow.co.uk/?p=64.
1009 rangeIntersectsNode : function(range, node)
1011 var nodeRange = node.ownerDocument.createRange();
1013 nodeRange.selectNode(node);
1015 nodeRange.selectNodeContents(node);
1018 var rangeStartRange = range.cloneRange();
1019 rangeStartRange.collapse(true);
1021 var rangeEndRange = range.cloneRange();
1022 rangeEndRange.collapse(false);
1024 var nodeStartRange = nodeRange.cloneRange();
1025 nodeStartRange.collapse(true);
1027 var nodeEndRange = nodeRange.cloneRange();
1028 nodeEndRange.collapse(false);
1030 return rangeStartRange.compareBoundaryPoints(
1031 Range.START_TO_START, nodeEndRange) == -1 &&
1032 rangeEndRange.compareBoundaryPoints(
1033 Range.START_TO_START, nodeStartRange) == 1;
1037 rangeCompareNode : function(range, node)
1039 var nodeRange = node.ownerDocument.createRange();
1041 nodeRange.selectNode(node);
1043 nodeRange.selectNodeContents(node);
1047 range.collapse(true);
1049 nodeRange.collapse(true);
1051 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1052 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1054 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1056 var nodeIsBefore = ss == 1;
1057 var nodeIsAfter = ee == -1;
1059 if (nodeIsBefore && nodeIsAfter)
1061 if (!nodeIsBefore && nodeIsAfter)
1062 return 1; //right trailed.
1064 if (nodeIsBefore && !nodeIsAfter)
1065 return 2; // left trailed.
1070 // private? - in a new class?
1071 cleanUpPaste : function()
1073 // cleans up the whole document..
1074 Roo.log('cleanuppaste');
1075 this.cleanUpChildren(this.doc.body);
1076 var clean = this.cleanWordChars(this.doc.body.innerHTML);
1077 if (clean != this.doc.body.innerHTML) {
1078 this.doc.body.innerHTML = clean;
1083 cleanWordChars : function(input) {// change the chars to hex code
1084 var he = Roo.form.HtmlEditor;
1087 Roo.each(he.swapCodes, function(sw) {
1088 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1090 output = output.replace(swapper, sw[1]);
1097 cleanUpChildren : function (n)
1099 if (!n.childNodes.length) {
1102 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1103 this.cleanUpChild(n.childNodes[i]);
1110 cleanUpChild : function (node)
1113 //console.log(node);
1114 if (node.nodeName == "#text") {
1115 // clean up silly Windows -- stuff?
1118 if (node.nodeName == "#comment") {
1119 node.parentNode.removeChild(node);
1120 // clean up silly Windows -- stuff?
1124 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
1126 node.parentNode.removeChild(node);
1131 var remove_keep_children= Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1;
1133 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1134 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1136 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1137 // remove_keep_children = true;
1140 if (remove_keep_children) {
1141 this.cleanUpChildren(node);
1142 // inserts everything just before this node...
1143 while (node.childNodes.length) {
1144 var cn = node.childNodes[0];
1145 node.removeChild(cn);
1146 node.parentNode.insertBefore(cn, node);
1148 node.parentNode.removeChild(node);
1152 if (!node.attributes || !node.attributes.length) {
1153 this.cleanUpChildren(node);
1157 function cleanAttr(n,v)
1160 if (v.match(/^\./) || v.match(/^\//)) {
1163 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1166 if (v.match(/^#/)) {
1169 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1170 node.removeAttribute(n);
1174 function cleanStyle(n,v)
1176 if (v.match(/expression/)) { //XSS?? should we even bother..
1177 node.removeAttribute(n);
1180 var cwhite = typeof(ed.cwhite) == 'undefined' ? Roo.form.HtmlEditor.cwhite : ed.cwhite;
1181 var cblack = typeof(ed.cblack) == 'undefined' ? Roo.form.HtmlEditor.cblack : ed.cblack;
1184 var parts = v.split(/;/);
1187 Roo.each(parts, function(p) {
1188 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1192 var l = p.split(':').shift().replace(/\s+/g,'');
1193 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1196 if ( cblack.indexOf(l) > -1) {
1197 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1198 //node.removeAttribute(n);
1202 // only allow 'c whitelisted system attributes'
1203 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1204 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1205 //node.removeAttribute(n);
1216 node.setAttribute(n, clean.join(';'));
1218 node.removeAttribute(n);
1224 for (var i = node.attributes.length-1; i > -1 ; i--) {
1225 var a = node.attributes[i];
1228 if (a.name.toLowerCase().substr(0,2)=='on') {
1229 node.removeAttribute(a.name);
1232 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1233 node.removeAttribute(a.name);
1236 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1237 cleanAttr(a.name,a.value); // fixme..
1240 if (a.name == 'style') {
1241 cleanStyle(a.name,a.value);
1244 /// clean up MS crap..
1245 // tecnically this should be a list of valid class'es..
1248 if (a.name == 'class') {
1249 if (a.value.match(/^Mso/)) {
1250 node.className = '';
1253 if (a.value.match(/body/)) {
1254 node.className = '';
1265 this.cleanUpChildren(node);
1271 // hide stuff that is not compatible
1289 * @cfg {String} fieldClass @hide
1292 * @cfg {String} focusClass @hide
1295 * @cfg {String} autoCreate @hide
1298 * @cfg {String} inputType @hide
1301 * @cfg {String} invalidClass @hide
1304 * @cfg {String} invalidText @hide
1307 * @cfg {String} msgFx @hide
1310 * @cfg {String} validateOnBlur @hide
1314 Roo.form.HtmlEditor.white = [
1315 'area', 'br', 'img', 'input', 'hr', 'wbr',
1317 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1318 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1319 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1320 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1321 'table', 'ul', 'xmp',
1323 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1326 'dir', 'menu', 'ol', 'ul', 'dl',
1332 Roo.form.HtmlEditor.black = [
1333 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1335 'base', 'basefont', 'bgsound', 'blink', 'body',
1336 'frame', 'frameset', 'head', 'html', 'ilayer',
1337 'iframe', 'layer', 'link', 'meta', 'object',
1338 'script', 'style' ,'title', 'xml' // clean later..
1340 Roo.form.HtmlEditor.clean = [
1341 'script', 'style', 'title', 'xml'
1343 Roo.form.HtmlEditor.remove = [
1348 Roo.form.HtmlEditor.ablack = [
1352 Roo.form.HtmlEditor.aclean = [
1353 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1357 Roo.form.HtmlEditor.pwhite= [
1358 'http', 'https', 'mailto'
1361 // white listed style attributes.
1362 Roo.form.HtmlEditor.cwhite= [
1363 // 'text-align', /// default is to allow most things..
1369 // black listed style attributes.
1370 Roo.form.HtmlEditor.cblack= [
1371 // 'font-size' -- this can be set by the project
1375 Roo.form.HtmlEditor.swapCodes =[