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 -= 5; // 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 'mouseup': this.onEditorEvent,
486 'dblclick': this.onEditorEvent,
487 'click': this.onEditorEvent,
488 'keyup': this.onEditorEvent,
493 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
495 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
496 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
498 this.initialized = true;
500 this.fireEvent('initialize', this);
505 onDestroy : function(){
511 for (var i =0; i < this.toolbars.length;i++) {
512 // fixme - ask toolbars for heights?
513 this.toolbars[i].onDestroy();
516 this.wrap.dom.innerHTML = '';
522 onFirstFocus : function(){
527 this.activated = true;
528 for (var i =0; i < this.toolbars.length;i++) {
529 this.toolbars[i].onFirstFocus();
532 if(Roo.isGecko){ // prevent silly gecko errors
534 var s = this.win.getSelection();
535 if(!s.focusNode || s.focusNode.nodeType != 3){
536 var r = s.getRangeAt(0);
537 r.selectNodeContents((this.doc.body || this.doc.documentElement));
542 this.execCmd('useCSS', true);
543 this.execCmd('styleWithCSS', false);
546 this.fireEvent('activate', this);
550 adjustFont: function(btn){
551 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
552 //if(Roo.isSafari){ // safari
555 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
556 if(Roo.isSafari){ // safari
557 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
558 v = (v < 10) ? 10 : v;
559 v = (v > 48) ? 48 : v;
560 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
565 v = Math.max(1, v+adjust);
567 this.execCmd('FontSize', v );
570 onEditorEvent : function(e){
571 this.fireEvent('editorevent', this, e);
572 // this.updateToolbar();
576 insertTag : function(tg)
578 // could be a bit smarter... -> wrap the current selected tRoo..
580 this.execCmd("formatblock", tg);
584 insertText : function(txt)
588 range = this.createRange();
589 range.deleteContents();
590 //alert(Sender.getAttribute('label'));
592 range.insertNode(this.doc.createTextNode(txt));
596 relayBtnCmd : function(btn){
597 this.relayCmd(btn.cmd);
601 * Executes a Midas editor command on the editor document and performs necessary focus and
602 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
603 * @param {String} cmd The Midas command
604 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
606 relayCmd : function(cmd, value){
608 this.execCmd(cmd, value);
609 this.fireEvent('editorevent', this);
610 //this.updateToolbar();
615 * Executes a Midas editor command directly on the editor document.
616 * For visual commands, you should use {@link #relayCmd} instead.
617 * <b>This should only be called after the editor is initialized.</b>
618 * @param {String} cmd The Midas command
619 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
621 execCmd : function(cmd, value){
622 this.doc.execCommand(cmd, false, value === undefined ? null : value);
628 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
630 * @param {String} text
632 insertAtCursor : function(text){
638 var r = this.doc.selection.createRange();
645 }else if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
647 this.execCmd('InsertHTML', text);
652 mozKeyPress : function(e){
654 var c = e.getCharCode(), cmd;
657 c = String.fromCharCode(c).toLowerCase();
668 this.cleanUpPaste.defer(100, this);
684 fixKeys : function(){ // load time branching for fastest keydown performance
687 var k = e.getKey(), r;
690 r = this.doc.selection.createRange();
693 r.pasteHTML('    ');
700 r = this.doc.selection.createRange();
702 var target = r.parentElement();
703 if(!target || target.tagName.toLowerCase() != 'li'){
705 r.pasteHTML('<br />');
711 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
712 this.cleanUpPaste.defer(100, this);
718 }else if(Roo.isOpera){
724 this.execCmd('InsertHTML','    ');
727 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
728 this.cleanUpPaste.defer(100, this);
733 }else if(Roo.isSafari){
739 this.execCmd('InsertText','\t');
743 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
744 this.cleanUpPaste.defer(100, this);
752 getAllAncestors: function()
754 var p = this.getSelectedNode();
757 a.push(p); // push blank onto stack..
758 p = this.getParentElement();
762 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
766 a.push(this.doc.body);
773 getSelection : function()
776 return Roo.isIE ? this.doc.selection : this.win.getSelection();
779 getSelectedNode: function()
781 // this may only work on Gecko!!!
783 // should we cache this!!!!
788 var range = this.createRange(this.getSelection());
791 var parent = range.parentElement();
793 var testRange = range.duplicate();
794 testRange.moveToElementText(parent);
795 if (testRange.inRange(range)) {
798 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
801 parent = parent.parentElement;
807 var ar = range.commonAncestorContainer.childNodes;
810 var other_nodes = [];
811 var has_other_nodes = false;
812 for (var i=0;i<ar.length;i++) {
813 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
816 // fullly contained node.
818 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
823 // probably selected..
824 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
825 other_nodes.push(ar[i]);
828 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
833 has_other_nodes = true;
835 if (!nodes.length && other_nodes.length) {
838 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
844 createRange: function(sel)
846 // this has strange effects when using with
847 // top toolbar - not sure if it's a great idea.
848 //this.editor.contentWindow.focus();
849 if (typeof sel != "undefined") {
851 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
853 return this.doc.createRange();
856 return this.doc.createRange();
859 getParentElement: function()
863 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
865 var range = this.createRange(sel);
868 var p = range.commonAncestorContainer;
869 while (p.nodeType == 3) { // text node
880 * Range intersection.. the hard stuff...
884 * [ -- selected range --- ]
888 * if end is before start or hits it. fail.
889 * if start is after end or hits it fail.
891 * if either hits (but other is outside. - then it's not
897 // @see http://www.thismuchiknow.co.uk/?p=64.
898 rangeIntersectsNode : function(range, node)
900 var nodeRange = node.ownerDocument.createRange();
902 nodeRange.selectNode(node);
904 nodeRange.selectNodeContents(node);
907 var rangeStartRange = range.cloneRange();
908 rangeStartRange.collapse(true);
910 var rangeEndRange = range.cloneRange();
911 rangeEndRange.collapse(false);
913 var nodeStartRange = nodeRange.cloneRange();
914 nodeStartRange.collapse(true);
916 var nodeEndRange = nodeRange.cloneRange();
917 nodeEndRange.collapse(false);
919 return rangeStartRange.compareBoundaryPoints(
920 Range.START_TO_START, nodeEndRange) == -1 &&
921 rangeEndRange.compareBoundaryPoints(
922 Range.START_TO_START, nodeStartRange) == 1;
926 rangeCompareNode : function(range, node)
928 var nodeRange = node.ownerDocument.createRange();
930 nodeRange.selectNode(node);
932 nodeRange.selectNodeContents(node);
936 var rangeStartRange = range.cloneRange();
937 rangeStartRange.collapse(true);
939 var rangeEndRange = range.cloneRange();
940 rangeEndRange.collapse(false);
942 var nodeStartRange = nodeRange.cloneRange();
943 nodeStartRange.collapse(true);
945 var nodeEndRange = nodeRange.cloneRange();
946 nodeEndRange.collapse(false);
949 var nodeIsBefore = rangeStartRange.compareBoundaryPoints(Range.START_TO_START, nodeEndRange) == 1;
950 var nodeIsAfter = rangeEndRange.compareBoundaryPoints(Range.END_TO_END, nodeStartRange) == -1;
952 if (nodeIsBefore && !nodeIsAfter)
954 if (!nodeIsBefore && nodeIsAfter)
956 if (nodeIsBefore && nodeIsAfter)
962 // private? - in a new class?
963 cleanUpPaste : function()
965 // cleans up the whole document..
966 // console.log('cleanuppaste');
967 this.cleanUpChildren(this.doc.body);
971 cleanUpChildren : function (n)
973 if (!n.childNodes.length) {
976 for (var i = n.childNodes.length-1; i > -1 ; i--) {
977 this.cleanUpChild(n.childNodes[i]);
984 cleanUpChild : function (node)
987 if (node.nodeName == "#text") {
988 // clean up silly Windows -- stuff?
991 if (node.nodeName == "#comment") {
992 node.parentNode.removeChild(node);
993 // clean up silly Windows -- stuff?
997 if (Roo.form.HtmlEditor.black.indexOf(node.tagName.toLowerCase()) > -1) {
999 node.parentNode.removeChild(node);
1003 if (Roo.form.HtmlEditor.remove.indexOf(node.tagName.toLowerCase()) > -1) {
1004 this.cleanUpChildren(node);
1005 // inserts everything just before this node...
1006 while (node.childNodes.length) {
1007 var cn = node.childNodes[0];
1008 node.removeChild(cn);
1009 node.parentNode.insertBefore(cn, node);
1011 node.parentNode.removeChild(node);
1015 if (!node.attributes || !node.attributes.length) {
1016 this.cleanUpChildren(node);
1020 function cleanAttr(n,v)
1023 if (v.match(/^\./) || v.match(/^\//)) {
1026 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1029 Roo.log("(REMOVE)"+ node.tagName +'.' + n + '=' + v);
1030 node.removeAttribute(n);
1034 function cleanStyle(n,v)
1036 if (v.match(/expression/)) { //XSS?? should we even bother..
1037 node.removeAttribute(n);
1042 var parts = v.split(/;/);
1043 Roo.each(parts, function(p) {
1044 p = p.replace(/\s+/g,'');
1048 var l = p.split(':').shift().replace(/\s+/g,'');
1050 if (Roo.form.HtmlEditor.cwhite.indexOf(l) < 0) {
1051 Roo.log('(REMOVE)' + node.tagName +'.' + n + ':'+l + '=' + v);
1052 node.removeAttribute(n);
1062 for (var i = node.attributes.length-1; i > -1 ; i--) {
1063 var a = node.attributes[i];
1065 if (Roo.form.HtmlEditor.ablack.indexOf(a.name.toLowerCase()) > -1) {
1066 node.removeAttribute(a.name);
1069 if (Roo.form.HtmlEditor.aclean.indexOf(a.name.toLowerCase()) > -1) {
1070 cleanAttr(a.name,a.value); // fixme..
1073 if (a.name == 'style') {
1074 cleanStyle(a.name,a.value);
1076 /// clean up MS crap..
1077 if (a.name == 'class') {
1078 if (a.value.match(/^Mso/)) {
1079 node.className = '';
1089 this.cleanUpChildren(node);
1095 // hide stuff that is not compatible
1113 * @cfg {String} fieldClass @hide
1116 * @cfg {String} focusClass @hide
1119 * @cfg {String} autoCreate @hide
1122 * @cfg {String} inputType @hide
1125 * @cfg {String} invalidClass @hide
1128 * @cfg {String} invalidText @hide
1131 * @cfg {String} msgFx @hide
1134 * @cfg {String} validateOnBlur @hide
1138 Roo.form.HtmlEditor.white = [
1139 'area', 'br', 'img', 'input', 'hr', 'wbr',
1141 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1142 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1143 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1144 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1145 'table', 'ul', 'xmp',
1147 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1150 'dir', 'menu', 'ol', 'ul', 'dl',
1156 Roo.form.HtmlEditor.black = [
1157 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1159 'base', 'basefont', 'bgsound', 'blink', 'body',
1160 'frame', 'frameset', 'head', 'html', 'ilayer',
1161 'iframe', 'layer', 'link', 'meta', 'object',
1162 'script', 'style' ,'title', 'xml' // clean later..
1164 Roo.form.HtmlEditor.clean = [
1165 'script', 'style', 'title', 'xml'
1167 Roo.form.HtmlEditor.remove = [
1172 Roo.form.HtmlEditor.ablack = [
1176 Roo.form.HtmlEditor.aclean = [
1177 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1181 Roo.form.HtmlEditor.pwhite= [
1182 'http', 'https', 'mailto'
1185 Roo.form.HtmlEditor.cwhite= [