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">' +
172 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
173 '</style></head><body></body></html>';
177 onRender : function(ct, position)
180 Roo.form.HtmlEditor.superclass.onRender.call(this, ct, position);
181 this.el.dom.style.border = '0 none';
182 this.el.dom.setAttribute('tabIndex', -1);
183 this.el.addClass('x-hidden');
184 if(Roo.isIE){ // fix IE 1px bogus margin
185 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
187 this.wrap = this.el.wrap({
188 cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'}
191 if (this.resizable) {
192 this.resizeEl = new Roo.Resizable(this.wrap, {
196 minHeight : this.height,
198 handles : this.resizable,
201 resize : function(r, w, h) {
202 _t.onResize(w,h); // -something
209 this.frameId = Roo.id();
211 this.createToolbar(this);
215 var iframe = this.wrap.createChild({
220 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
224 // console.log(iframe);
225 //this.wrap.dom.appendChild(iframe);
227 this.iframe = iframe.dom;
231 this.doc.designMode = 'on';
234 this.doc.write(this.getDocMarkup());
238 var task = { // must defer to wait for browser to be ready
240 //console.log("run task?" + this.doc.readyState);
242 if(this.doc.body || this.doc.readyState == 'complete'){
244 this.doc.designMode="on";
248 Roo.TaskMgr.stop(task);
249 this.initEditor.defer(10, this);
256 Roo.TaskMgr.start(task);
259 this.setSize(this.wrap.getSize());
262 this.resizeEl.resizeTo.defer(100, this.resizeEl,[ this.width,this.height ] );
263 // should trigger onReize..
268 onResize : function(w, h)
270 //Roo.log('resize: ' +w + ',' + h );
271 Roo.form.HtmlEditor.superclass.onResize.apply(this, arguments);
272 if(this.el && this.iframe){
273 if(typeof w == 'number'){
274 var aw = w - this.wrap.getFrameWidth('lr');
275 this.el.setWidth(this.adjustWidth('textarea', aw));
276 this.iframe.style.width = aw + 'px';
278 if(typeof h == 'number'){
280 for (var i =0; i < this.toolbars.length;i++) {
281 // fixme - ask toolbars for heights?
282 tbh += this.toolbars[i].tb.el.getHeight();
283 if (this.toolbars[i].footer) {
284 tbh += this.toolbars[i].footer.el.getHeight();
291 var ah = h - this.wrap.getFrameWidth('tb') - tbh;// this.tb.el.getHeight();
292 ah -= 5; // knock a few pixes off for look..
293 this.el.setHeight(this.adjustWidth('textarea', ah));
294 this.iframe.style.height = ah + 'px';
296 (this.doc.body || this.doc.documentElement).style.height = (ah - (this.iframePad*2)) + 'px';
303 * Toggles the editor between standard and source edit mode.
304 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
306 toggleSourceEdit : function(sourceEditMode){
308 this.sourceEditMode = sourceEditMode === true;
310 if(this.sourceEditMode){
313 this.iframe.className = 'x-hidden';
314 this.el.removeClass('x-hidden');
315 this.el.dom.removeAttribute('tabIndex');
320 this.iframe.className = '';
321 this.el.addClass('x-hidden');
322 this.el.dom.setAttribute('tabIndex', -1);
325 this.setSize(this.wrap.getSize());
326 this.fireEvent('editmodechange', this, this.sourceEditMode);
329 // private used internally
330 createLink : function(){
331 var url = prompt(this.createLinkText, this.defaultLinkValue);
332 if(url && url != 'http:/'+'/'){
333 this.relayCmd('createlink', url);
337 // private (for BoxComponent)
338 adjustSize : Roo.BoxComponent.prototype.adjustSize,
340 // private (for BoxComponent)
341 getResizeEl : function(){
345 // private (for BoxComponent)
346 getPositionEl : function(){
351 initEvents : function(){
352 this.originalValue = this.getValue();
356 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
359 markInvalid : Roo.emptyFn,
361 * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide
364 clearInvalid : Roo.emptyFn,
366 setValue : function(v){
367 Roo.form.HtmlEditor.superclass.setValue.call(this, v);
372 * Protected method that will not generally be called directly. If you need/want
373 * custom HTML cleanup, this is the method you should override.
374 * @param {String} html The HTML to be cleaned
375 * return {String} The cleaned HTML
377 cleanHtml : function(html){
380 if(Roo.isSafari){ // strip safari nonsense
381 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
384 if(html == ' '){
391 * Protected method that will not generally be called directly. Syncs the contents
392 * of the editor iframe with the textarea.
394 syncValue : function(){
395 if(this.initialized){
396 var bd = (this.doc.body || this.doc.documentElement);
398 var html = bd.innerHTML;
400 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
401 var m = bs.match(/text-align:(.*?);/i);
403 html = '<div style="'+m[0]+'">' + html + '</div>';
406 html = this.cleanHtml(html);
407 if(this.fireEvent('beforesync', this, html) !== false){
408 this.el.dom.value = html;
409 this.fireEvent('sync', this, html);
415 * Protected method that will not generally be called directly. Pushes the value of the textarea
416 * into the iframe editor.
418 pushValue : function(){
419 if(this.initialized){
420 var v = this.el.dom.value;
425 if(this.fireEvent('beforepush', this, v) !== false){
426 var d = (this.doc.body || this.doc.documentElement);
429 this.el.dom.value = d.innerHTML;
430 this.fireEvent('push', this, v);
436 deferFocus : function(){
437 this.focus.defer(10, this);
442 if(this.win && !this.sourceEditMode){
449 assignDocWin: function()
451 var iframe = this.iframe;
454 this.doc = iframe.contentWindow.document;
455 this.win = iframe.contentWindow;
457 if (!Roo.get(this.frameId)) {
460 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
461 this.win = Roo.get(this.frameId).dom.contentWindow;
466 initEditor : function(){
467 //console.log("INIT EDITOR");
472 this.doc.designMode="on";
474 this.doc.write(this.getDocMarkup());
477 var dbody = (this.doc.body || this.doc.documentElement);
478 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
479 // this copies styles from the containing element into thsi one..
480 // not sure why we need all of this..
481 var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
482 ss['background-attachment'] = 'fixed'; // w3c
483 dbody.bgProperties = 'fixed'; // ie
484 Roo.DomHelper.applyStyles(dbody, ss);
485 Roo.EventManager.on(this.doc, {
486 //'mousedown': this.onEditorEvent,
487 'mouseup': this.onEditorEvent,
488 'dblclick': this.onEditorEvent,
489 'click': this.onEditorEvent,
490 'keyup': this.onEditorEvent,
495 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
497 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
498 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
500 this.initialized = true;
502 this.fireEvent('initialize', this);
507 onDestroy : function(){
513 for (var i =0; i < this.toolbars.length;i++) {
514 // fixme - ask toolbars for heights?
515 this.toolbars[i].onDestroy();
518 this.wrap.dom.innerHTML = '';
524 onFirstFocus : function(){
529 this.activated = true;
530 for (var i =0; i < this.toolbars.length;i++) {
531 this.toolbars[i].onFirstFocus();
534 if(Roo.isGecko){ // prevent silly gecko errors
536 var s = this.win.getSelection();
537 if(!s.focusNode || s.focusNode.nodeType != 3){
538 var r = s.getRangeAt(0);
539 r.selectNodeContents((this.doc.body || this.doc.documentElement));
544 this.execCmd('useCSS', true);
545 this.execCmd('styleWithCSS', false);
548 this.fireEvent('activate', this);
552 adjustFont: function(btn){
553 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
554 //if(Roo.isSafari){ // safari
557 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
558 if(Roo.isSafari){ // safari
559 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
560 v = (v < 10) ? 10 : v;
561 v = (v > 48) ? 48 : v;
562 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
567 v = Math.max(1, v+adjust);
569 this.execCmd('FontSize', v );
572 onEditorEvent : function(e){
573 this.fireEvent('editorevent', this, e);
574 // this.updateToolbar();
578 insertTag : function(tg)
580 // could be a bit smarter... -> wrap the current selected tRoo..
582 this.execCmd("formatblock", tg);
586 insertText : function(txt)
590 range = this.createRange();
591 range.deleteContents();
592 //alert(Sender.getAttribute('label'));
594 range.insertNode(this.doc.createTextNode(txt));
598 relayBtnCmd : function(btn){
599 this.relayCmd(btn.cmd);
603 * Executes a Midas editor command on the editor document and performs necessary focus and
604 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
605 * @param {String} cmd The Midas command
606 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
608 relayCmd : function(cmd, value){
610 this.execCmd(cmd, value);
611 this.fireEvent('editorevent', this);
612 //this.updateToolbar();
617 * Executes a Midas editor command directly on the editor document.
618 * For visual commands, you should use {@link #relayCmd} instead.
619 * <b>This should only be called after the editor is initialized.</b>
620 * @param {String} cmd The Midas command
621 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
623 execCmd : function(cmd, value){
624 this.doc.execCommand(cmd, false, value === undefined ? null : value);
630 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
632 * @param {String} text
634 insertAtCursor : function(text){
640 var r = this.doc.selection.createRange();
647 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
649 this.execCmd('InsertHTML', text);
654 mozKeyPress : function(e){
656 var c = e.getCharCode(), cmd;
659 c = String.fromCharCode(c).toLowerCase();
670 this.cleanUpPaste.defer(100, this);
686 fixKeys : function(){ // load time branching for fastest keydown performance
689 var k = e.getKey(), r;
692 r = this.doc.selection.createRange();
695 r.pasteHTML('    ');
702 r = this.doc.selection.createRange();
704 var target = r.parentElement();
705 if(!target || target.tagName.toLowerCase() != 'li'){
707 r.pasteHTML('<br />');
713 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
714 this.cleanUpPaste.defer(100, this);
720 }else if(Roo.isOpera){
726 this.execCmd('InsertHTML','    ');
729 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
730 this.cleanUpPaste.defer(100, this);
735 }else if(Roo.isSafari){
741 this.execCmd('InsertText','\t');
745 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
746 this.cleanUpPaste.defer(100, this);
754 getAllAncestors: function()
756 var p = this.getSelectedNode();
759 a.push(p); // push blank onto stack..
760 p = this.getParentElement();
764 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
768 a.push(this.doc.body);
775 getSelection : function()
778 return Roo.isIE ? this.doc.selection : this.win.getSelection();
781 getSelectedNode: function()
783 // this may only work on Gecko!!!
785 // should we cache this!!!!
790 var range = this.createRange(this.getSelection());
793 var parent = range.parentElement();
795 var testRange = range.duplicate();
796 testRange.moveToElementText(parent);
797 if (testRange.inRange(range)) {
800 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
803 parent = parent.parentElement;
808 // is ancestor a text element.
809 var ac = range.commonAncestorContainer;
810 if (ac.nodeType == 3) {
814 var ar = ac.childNodes;
817 var other_nodes = [];
818 var has_other_nodes = false;
819 for (var i=0;i<ar.length;i++) {
820 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
823 // fullly contained node.
825 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
830 // probably selected..
831 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
832 other_nodes.push(ar[i]);
836 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
841 has_other_nodes = true;
843 if (!nodes.length && other_nodes.length) {
846 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
852 createRange: function(sel)
854 // this has strange effects when using with
855 // top toolbar - not sure if it's a great idea.
856 //this.editor.contentWindow.focus();
857 if (typeof sel != "undefined") {
859 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
861 return this.doc.createRange();
864 return this.doc.createRange();
867 getParentElement: function()
871 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
873 var range = this.createRange(sel);
876 var p = range.commonAncestorContainer;
877 while (p.nodeType == 3) { // text node
888 * Range intersection.. the hard stuff...
892 * [ -- selected range --- ]
896 * if end is before start or hits it. fail.
897 * if start is after end or hits it fail.
899 * if either hits (but other is outside. - then it's not
905 // @see http://www.thismuchiknow.co.uk/?p=64.
906 rangeIntersectsNode : function(range, node)
908 var nodeRange = node.ownerDocument.createRange();
910 nodeRange.selectNode(node);
912 nodeRange.selectNodeContents(node);
915 var rangeStartRange = range.cloneRange();
916 rangeStartRange.collapse(true);
918 var rangeEndRange = range.cloneRange();
919 rangeEndRange.collapse(false);
921 var nodeStartRange = nodeRange.cloneRange();
922 nodeStartRange.collapse(true);
924 var nodeEndRange = nodeRange.cloneRange();
925 nodeEndRange.collapse(false);
927 return rangeStartRange.compareBoundaryPoints(
928 Range.START_TO_START, nodeEndRange) == -1 &&
929 rangeEndRange.compareBoundaryPoints(
930 Range.START_TO_START, nodeStartRange) == 1;
934 rangeCompareNode : function(range, node)
936 var nodeRange = node.ownerDocument.createRange();
938 nodeRange.selectNode(node);
940 nodeRange.selectNodeContents(node);
944 range.collapse(true);
946 nodeRange.collapse(true);
948 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
949 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
951 Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
953 var nodeIsBefore = ss == 1;
954 var nodeIsAfter = ee == -1;
956 if (nodeIsBefore && nodeIsAfter)
958 if (!nodeIsBefore && nodeIsAfter)
959 return 1; //right trailed.
961 if (nodeIsBefore && !nodeIsAfter)
962 return 2; // left trailed.
967 // private? - in a new class?
968 cleanUpPaste : function()
970 // cleans up the whole document..
971 // console.log('cleanuppaste');
972 this.cleanUpChildren(this.doc.body);
976 cleanUpChildren : function (n)
978 if (!n.childNodes.length) {
981 for (var i = n.childNodes.length-1; i > -1 ; i--) {
982 this.cleanUpChild(n.childNodes[i]);
989 cleanUpChild : function (node)
992 if (node.nodeName == "#text") {
993 // clean up silly Windows -- stuff?
996 if (node.nodeName == "#comment") {
997 node.parentNode.removeChild(node);
998 // clean up silly Windows -- stuff?
1002 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
1004 node.parentNode.removeChild(node);
1008 if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
1009 this.cleanUpChildren(node);
1010 // inserts everything just before this node...
1011 while (node.childNodes.length) {
1012 var cn = node.childNodes[0];
1013 node.removeChild(cn);
1014 node.parentNode.insertBefore(cn, node);
1016 node.parentNode.removeChild(node);
1020 if (!node.attributes || !node.attributes.length) {
1021 this.cleanUpChildren(node);
1025 function cleanAttr(n,v)
1028 if (v.match(/^\./) || v.match(/^\//)) {
1031 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1034 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
1035 node.removeAttribute(n);
1039 function cleanStyle(n,v)
1041 if (v.match(/expression/)) { //XSS?? should we even bother..
1042 node.removeAttribute(n);
1047 var parts = v.split(/;/);
1048 Roo.each(parts, function(p) {
1049 p = p.replace(/\s+/g,'');
1053 var l = p.split(':').shift().replace(/\s+/g,'');
1055 // only allow 'c whitelisted system attributes'
1056 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1057 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1058 node.removeAttribute(n);
1068 for (var i = node.attributes.length-1; i > -1 ; i--) {
1069 var a = node.attributes[i];
1071 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1072 node.removeAttribute(a.name);
1075 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1076 cleanAttr(a.name,a.value); // fixme..
1079 if (a.name == 'style') {
1080 cleanStyle(a.name,a.value);
1082 /// clean up MS crap..
1083 if (a.name == 'class') {
1084 if (a.value.match(/^Mso/)) {
1085 node.className = '';
1095 this.cleanUpChildren(node);
1101 // hide stuff that is not compatible
1119 * @cfg {String} fieldClass @hide
1122 * @cfg {String} focusClass @hide
1125 * @cfg {String} autoCreate @hide
1128 * @cfg {String} inputType @hide
1131 * @cfg {String} invalidClass @hide
1134 * @cfg {String} invalidText @hide
1137 * @cfg {String} msgFx @hide
1140 * @cfg {String} validateOnBlur @hide
1144 Roo.form.HtmlEditor.white = [
1145 'area', 'br', 'img', 'input', 'hr', 'wbr',
1147 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1148 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1149 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1150 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1151 'table', 'ul', 'xmp',
1153 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1156 'dir', 'menu', 'ol', 'ul', 'dl',
1162 Roo.form.HtmlEditor.black = [
1163 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1165 'base', 'basefont', 'bgsound', 'blink', 'body',
1166 'frame', 'frameset', 'head', 'html', 'ilayer',
1167 'iframe', 'layer', 'link', 'meta', 'object',
1168 'script', 'style' ,'title', 'xml' // clean later..
1170 Roo.form.HtmlEditor.clean = [
1171 'script', 'style', 'title', 'xml'
1173 Roo.form.HtmlEditor.remove = [
1178 Roo.form.HtmlEditor.ablack = [
1182 Roo.form.HtmlEditor.aclean = [
1183 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1187 Roo.form.HtmlEditor.pwhite= [
1188 'http', 'https', 'mailto'
1191 // white listed style attributes.
1192 Roo.form.HtmlEditor.cwhite= [