1 //<script type="text/javascript">
4 * Based Ext JS Library 1.1.1
5 * Copyright(c) 2006-2007, Ext JS, LLC.
11 * @class Roo.HtmlEditorCore
12 * @extends Roo.Component
13 * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
15 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
18 Roo.HtmlEditorCore = function(config){
21 Roo.HtmlEditorCore.superclass.constructor.call(this, config);
27 * Fires when the editor is fully initialized (including the iframe)
28 * @param {Roo.HtmlEditorCore} this
33 * Fires when the editor is first receives the focus. Any insertion must wait
34 * until after this event.
35 * @param {Roo.HtmlEditorCore} this
40 * Fires before the textarea is updated with content from the editor iframe. Return false
42 * @param {Roo.HtmlEditorCore} this
43 * @param {String} html
48 * Fires before the iframe editor is updated with content from the textarea. Return false
50 * @param {Roo.HtmlEditorCore} this
51 * @param {String} html
56 * Fires when the textarea is updated with content from the editor iframe.
57 * @param {Roo.HtmlEditorCore} this
58 * @param {String} html
63 * Fires when the iframe editor is updated with content from the textarea.
64 * @param {Roo.HtmlEditorCore} this
65 * @param {String} html
71 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72 * @param {Roo.HtmlEditorCore} this
78 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80 // defaults : white / black...
81 this.applyBlacklists();
88 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
92 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
98 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
103 * @cfg {Number} height (in pixels)
107 * @cfg {Number} width (in pixels)
112 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
120 // private properties
121 validationEvent : false,
125 sourceEditMode : false,
126 onFocus : Roo.emptyFn,
132 // blacklist + whitelisted elements..
139 * Protected method that will not generally be called directly. It
140 * is called when the editor initializes the iframe with HTML contents. Override this method if you
141 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
143 getDocMarkup : function(){
147 // inherit styels from page...??
148 if (this.stylesheets === false) {
150 Roo.get(document.head).select('style').each(function(node) {
151 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
154 Roo.get(document.head).select('link').each(function(node) {
155 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
158 } else if (!this.stylesheets.length) {
160 st = '<style type="text/css">' +
161 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
167 st += '<style type="text/css">' +
168 'IMG { cursor: pointer } ' +
172 return '<html><head>' + st +
173 //<style type="text/css">' +
174 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
176 ' </head><body class="roo-htmleditor-body"></body></html>';
180 onRender : function(ct, position)
183 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
184 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
187 this.el.dom.style.border = '0 none';
188 this.el.dom.setAttribute('tabIndex', -1);
189 this.el.addClass('x-hidden hide');
193 if(Roo.isIE){ // fix IE 1px bogus margin
194 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
198 this.frameId = Roo.id();
202 var iframe = this.owner.wrap.createChild({
204 cls: 'form-control', // bootstrap..
208 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
213 this.iframe = iframe.dom;
217 this.doc.designMode = 'on';
220 this.doc.write(this.getDocMarkup());
224 var task = { // must defer to wait for browser to be ready
226 //console.log("run task?" + this.doc.readyState);
228 if(this.doc.body || this.doc.readyState == 'complete'){
230 this.doc.designMode="on";
234 Roo.TaskMgr.stop(task);
235 this.initEditor.defer(10, this);
242 Roo.TaskMgr.start(task);
247 onResize : function(w, h)
249 Roo.log('resize: ' +w + ',' + h );
250 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
254 if(typeof w == 'number'){
256 this.iframe.style.width = w + 'px';
258 if(typeof h == 'number'){
260 this.iframe.style.height = h + 'px';
262 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
269 * Toggles the editor between standard and source edit mode.
270 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
272 toggleSourceEdit : function(sourceEditMode){
274 this.sourceEditMode = sourceEditMode === true;
276 if(this.sourceEditMode){
278 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
281 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
282 //this.iframe.className = '';
285 //this.setSize(this.owner.wrap.getSize());
286 //this.fireEvent('editmodechange', this, this.sourceEditMode);
293 * Protected method that will not generally be called directly. If you need/want
294 * custom HTML cleanup, this is the method you should override.
295 * @param {String} html The HTML to be cleaned
296 * return {String} The cleaned HTML
298 cleanHtml : function(html){
301 if(Roo.isSafari){ // strip safari nonsense
302 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
305 if(html == ' '){
312 * HTML Editor -> Textarea
313 * Protected method that will not generally be called directly. Syncs the contents
314 * of the editor iframe with the textarea.
316 syncValue : function(){
317 if(this.initialized){
318 var bd = (this.doc.body || this.doc.documentElement);
319 //this.cleanUpPaste(); -- this is done else where and causes havoc..
320 var html = bd.innerHTML;
322 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
323 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
325 html = '<div style="'+m[0]+'">' + html + '</div>';
328 html = this.cleanHtml(html);
329 // fix up the special chars.. normaly like back quotes in word...
330 // however we do not want to do this with chinese..
331 html = html.replace(/([\x80-\uffff])/g, function (a, b) {
332 var cc = b.charCodeAt();
334 (cc >= 0x4E00 && cc < 0xA000 ) ||
335 (cc >= 0x3400 && cc < 0x4E00 ) ||
336 (cc >= 0xf900 && cc < 0xfb00 )
342 if(this.owner.fireEvent('beforesync', this, html) !== false){
343 this.el.dom.value = html;
344 this.owner.fireEvent('sync', this, html);
350 * Protected method that will not generally be called directly. Pushes the value of the textarea
351 * into the iframe editor.
353 pushValue : function(){
354 if(this.initialized){
355 var v = this.el.dom.value.trim();
361 if(this.owner.fireEvent('beforepush', this, v) !== false){
362 var d = (this.doc.body || this.doc.documentElement);
365 this.el.dom.value = d.innerHTML;
366 this.owner.fireEvent('push', this, v);
372 deferFocus : function(){
373 this.focus.defer(10, this);
378 if(this.win && !this.sourceEditMode){
385 assignDocWin: function()
387 var iframe = this.iframe;
390 this.doc = iframe.contentWindow.document;
391 this.win = iframe.contentWindow;
393 // if (!Roo.get(this.frameId)) {
396 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
397 // this.win = Roo.get(this.frameId).dom.contentWindow;
399 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
403 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
404 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
409 initEditor : function(){
410 //console.log("INIT EDITOR");
415 this.doc.designMode="on";
417 this.doc.write(this.getDocMarkup());
420 var dbody = (this.doc.body || this.doc.documentElement);
421 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
422 // this copies styles from the containing element into thsi one..
423 // not sure why we need all of this..
424 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
426 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
427 //ss['background-attachment'] = 'fixed'; // w3c
428 dbody.bgProperties = 'fixed'; // ie
429 //Roo.DomHelper.applyStyles(dbody, ss);
430 Roo.EventManager.on(this.doc, {
431 //'mousedown': this.onEditorEvent,
432 'mouseup': this.onEditorEvent,
433 'dblclick': this.onEditorEvent,
434 'click': this.onEditorEvent,
435 'keyup': this.onEditorEvent,
440 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
442 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
443 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
445 this.initialized = true;
447 this.owner.fireEvent('initialize', this);
452 onDestroy : function(){
458 //for (var i =0; i < this.toolbars.length;i++) {
459 // // fixme - ask toolbars for heights?
460 // this.toolbars[i].onDestroy();
463 //this.wrap.dom.innerHTML = '';
464 //this.wrap.remove();
469 onFirstFocus : function(){
474 this.activated = true;
477 if(Roo.isGecko){ // prevent silly gecko errors
479 var s = this.win.getSelection();
480 if(!s.focusNode || s.focusNode.nodeType != 3){
481 var r = s.getRangeAt(0);
482 r.selectNodeContents((this.doc.body || this.doc.documentElement));
487 this.execCmd('useCSS', true);
488 this.execCmd('styleWithCSS', false);
491 this.owner.fireEvent('activate', this);
495 adjustFont: function(btn){
496 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
497 //if(Roo.isSafari){ // safari
500 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
501 if(Roo.isSafari){ // safari
502 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
503 v = (v < 10) ? 10 : v;
504 v = (v > 48) ? 48 : v;
505 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
510 v = Math.max(1, v+adjust);
512 this.execCmd('FontSize', v );
515 onEditorEvent : function(e)
517 this.owner.fireEvent('editorevent', this, e);
518 // this.updateToolbar();
519 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
522 insertTag : function(tg)
524 // could be a bit smarter... -> wrap the current selected tRoo..
525 if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
527 range = this.createRange(this.getSelection());
528 var wrappingNode = this.doc.createElement(tg.toLowerCase());
529 wrappingNode.appendChild(range.extractContents());
530 range.insertNode(wrappingNode);
537 this.execCmd("formatblock", tg);
541 insertText : function(txt)
545 var range = this.createRange();
546 range.deleteContents();
547 //alert(Sender.getAttribute('label'));
549 range.insertNode(this.doc.createTextNode(txt));
555 * Executes a Midas editor command on the editor document and performs necessary focus and
556 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
557 * @param {String} cmd The Midas command
558 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
560 relayCmd : function(cmd, value){
562 this.execCmd(cmd, value);
563 this.owner.fireEvent('editorevent', this);
564 //this.updateToolbar();
565 this.owner.deferFocus();
569 * Executes a Midas editor command directly on the editor document.
570 * For visual commands, you should use {@link #relayCmd} instead.
571 * <b>This should only be called after the editor is initialized.</b>
572 * @param {String} cmd The Midas command
573 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
575 execCmd : function(cmd, value){
576 this.doc.execCommand(cmd, false, value === undefined ? null : value);
583 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
585 * @param {String} text | dom node..
587 insertAtCursor : function(text)
596 var r = this.doc.selection.createRange();
607 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
611 // from jquery ui (MIT licenced)
615 if (win.getSelection && win.getSelection().getRangeAt) {
616 range = win.getSelection().getRangeAt(0);
617 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
618 range.insertNode(node);
619 } else if (win.document.selection && win.document.selection.createRange) {
620 // no firefox support
621 var txt = typeof(text) == 'string' ? text : text.outerHTML;
622 win.document.selection.createRange().pasteHTML(txt);
624 // no firefox support
625 var txt = typeof(text) == 'string' ? text : text.outerHTML;
626 this.execCmd('InsertHTML', txt);
635 mozKeyPress : function(e){
637 var c = e.getCharCode(), cmd;
640 c = String.fromCharCode(c).toLowerCase();
654 this.cleanUpPaste.defer(100, this);
670 fixKeys : function(){ // load time branching for fastest keydown performance
673 var k = e.getKey(), r;
676 r = this.doc.selection.createRange();
679 r.pasteHTML('    ');
686 r = this.doc.selection.createRange();
688 var target = r.parentElement();
689 if(!target || target.tagName.toLowerCase() != 'li'){
691 r.pasteHTML('<br />');
697 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
698 this.cleanUpPaste.defer(100, this);
704 }else if(Roo.isOpera){
710 this.execCmd('InsertHTML','    ');
713 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
714 this.cleanUpPaste.defer(100, this);
719 }else if(Roo.isSafari){
725 this.execCmd('InsertText','\t');
729 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
730 this.cleanUpPaste.defer(100, this);
738 getAllAncestors: function()
740 var p = this.getSelectedNode();
743 a.push(p); // push blank onto stack..
744 p = this.getParentElement();
748 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
752 a.push(this.doc.body);
759 getSelection : function()
762 return Roo.isIE ? this.doc.selection : this.win.getSelection();
765 getSelectedNode: function()
767 // this may only work on Gecko!!!
769 // should we cache this!!!!
774 var range = this.createRange(this.getSelection()).cloneRange();
777 var parent = range.parentElement();
779 var testRange = range.duplicate();
780 testRange.moveToElementText(parent);
781 if (testRange.inRange(range)) {
784 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
787 parent = parent.parentElement;
792 // is ancestor a text element.
793 var ac = range.commonAncestorContainer;
794 if (ac.nodeType == 3) {
798 var ar = ac.childNodes;
801 var other_nodes = [];
802 var has_other_nodes = false;
803 for (var i=0;i<ar.length;i++) {
804 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
807 // fullly contained node.
809 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
814 // probably selected..
815 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
816 other_nodes.push(ar[i]);
820 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
825 has_other_nodes = true;
827 if (!nodes.length && other_nodes.length) {
830 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
836 createRange: function(sel)
838 // this has strange effects when using with
839 // top toolbar - not sure if it's a great idea.
840 //this.editor.contentWindow.focus();
841 if (typeof sel != "undefined") {
843 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
845 return this.doc.createRange();
848 return this.doc.createRange();
851 getParentElement: function()
855 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
857 var range = this.createRange(sel);
860 var p = range.commonAncestorContainer;
861 while (p.nodeType == 3) { // text node
872 * Range intersection.. the hard stuff...
876 * [ -- selected range --- ]
880 * if end is before start or hits it. fail.
881 * if start is after end or hits it fail.
883 * if either hits (but other is outside. - then it's not
889 // @see http://www.thismuchiknow.co.uk/?p=64.
890 rangeIntersectsNode : function(range, node)
892 var nodeRange = node.ownerDocument.createRange();
894 nodeRange.selectNode(node);
896 nodeRange.selectNodeContents(node);
899 var rangeStartRange = range.cloneRange();
900 rangeStartRange.collapse(true);
902 var rangeEndRange = range.cloneRange();
903 rangeEndRange.collapse(false);
905 var nodeStartRange = nodeRange.cloneRange();
906 nodeStartRange.collapse(true);
908 var nodeEndRange = nodeRange.cloneRange();
909 nodeEndRange.collapse(false);
911 return rangeStartRange.compareBoundaryPoints(
912 Range.START_TO_START, nodeEndRange) == -1 &&
913 rangeEndRange.compareBoundaryPoints(
914 Range.START_TO_START, nodeStartRange) == 1;
918 rangeCompareNode : function(range, node)
920 var nodeRange = node.ownerDocument.createRange();
922 nodeRange.selectNode(node);
924 nodeRange.selectNodeContents(node);
928 range.collapse(true);
930 nodeRange.collapse(true);
932 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
933 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
935 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
937 var nodeIsBefore = ss == 1;
938 var nodeIsAfter = ee == -1;
940 if (nodeIsBefore && nodeIsAfter) {
943 if (!nodeIsBefore && nodeIsAfter) {
944 return 1; //right trailed.
947 if (nodeIsBefore && !nodeIsAfter) {
948 return 2; // left trailed.
954 // private? - in a new class?
955 cleanUpPaste : function()
957 // cleans up the whole document..
958 Roo.log('cleanuppaste');
960 this.cleanUpChildren(this.doc.body);
961 var clean = this.cleanWordChars(this.doc.body.innerHTML);
962 if (clean != this.doc.body.innerHTML) {
963 this.doc.body.innerHTML = clean;
968 cleanWordChars : function(input) {// change the chars to hex code
969 var he = Roo.HtmlEditorCore;
972 Roo.each(he.swapCodes, function(sw) {
973 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
975 output = output.replace(swapper, sw[1]);
982 cleanUpChildren : function (n)
984 if (!n.childNodes.length) {
987 for (var i = n.childNodes.length-1; i > -1 ; i--) {
988 this.cleanUpChild(n.childNodes[i]);
995 cleanUpChild : function (node)
999 if (node.nodeName == "#text") {
1000 // clean up silly Windows -- stuff?
1003 if (node.nodeName == "#comment") {
1004 node.parentNode.removeChild(node);
1005 // clean up silly Windows -- stuff?
1008 var lcname = node.tagName.toLowerCase();
1009 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1010 // whitelist of tags..
1012 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1014 node.parentNode.removeChild(node);
1019 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1021 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1022 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1024 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1025 // remove_keep_children = true;
1028 if (remove_keep_children) {
1029 this.cleanUpChildren(node);
1030 // inserts everything just before this node...
1031 while (node.childNodes.length) {
1032 var cn = node.childNodes[0];
1033 node.removeChild(cn);
1034 node.parentNode.insertBefore(cn, node);
1036 node.parentNode.removeChild(node);
1040 if (!node.attributes || !node.attributes.length) {
1041 this.cleanUpChildren(node);
1045 function cleanAttr(n,v)
1048 if (v.match(/^\./) || v.match(/^\//)) {
1051 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1054 if (v.match(/^#/)) {
1057 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1058 node.removeAttribute(n);
1062 var cwhite = this.cwhite;
1063 var cblack = this.cblack;
1065 function cleanStyle(n,v)
1067 if (v.match(/expression/)) { //XSS?? should we even bother..
1068 node.removeAttribute(n);
1072 var parts = v.split(/;/);
1075 Roo.each(parts, function(p) {
1076 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1080 var l = p.split(':').shift().replace(/\s+/g,'');
1081 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1083 if ( cwhite.length && cblack.indexOf(l) > -1) {
1084 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1085 //node.removeAttribute(n);
1089 // only allow 'c whitelisted system attributes'
1090 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1091 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1092 //node.removeAttribute(n);
1103 node.setAttribute(n, clean.join(';'));
1105 node.removeAttribute(n);
1111 for (var i = node.attributes.length-1; i > -1 ; i--) {
1112 var a = node.attributes[i];
1115 if (a.name.toLowerCase().substr(0,2)=='on') {
1116 node.removeAttribute(a.name);
1119 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1120 node.removeAttribute(a.name);
1123 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1124 cleanAttr(a.name,a.value); // fixme..
1127 if (a.name == 'style') {
1128 cleanStyle(a.name,a.value);
1131 /// clean up MS crap..
1132 // tecnically this should be a list of valid class'es..
1135 if (a.name == 'class') {
1136 if (a.value.match(/^Mso/)) {
1137 node.className = '';
1140 if (a.value.match(/body/)) {
1141 node.className = '';
1152 this.cleanUpChildren(node);
1158 * Clean up MS wordisms...
1160 cleanWord : function(node)
1165 this.cleanWord(this.doc.body);
1168 if (node.nodeName == "#text") {
1169 // clean up silly Windows -- stuff?
1172 if (node.nodeName == "#comment") {
1173 node.parentNode.removeChild(node);
1174 // clean up silly Windows -- stuff?
1178 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1179 node.parentNode.removeChild(node);
1183 // remove - but keep children..
1184 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1185 while (node.childNodes.length) {
1186 var cn = node.childNodes[0];
1187 node.removeChild(cn);
1188 node.parentNode.insertBefore(cn, node);
1190 node.parentNode.removeChild(node);
1191 this.iterateChildren(node, this.cleanWord);
1195 if (node.className.length) {
1197 var cn = node.className.split(/\W+/);
1199 Roo.each(cn, function(cls) {
1200 if (cls.match(/Mso[a-zA-Z]+/)) {
1205 node.className = cna.length ? cna.join(' ') : '';
1207 node.removeAttribute("class");
1211 if (node.hasAttribute("lang")) {
1212 node.removeAttribute("lang");
1215 if (node.hasAttribute("style")) {
1217 var styles = node.getAttribute("style").split(";");
1219 Roo.each(styles, function(s) {
1220 if (!s.match(/:/)) {
1223 var kv = s.split(":");
1224 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1227 // what ever is left... we allow.
1230 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1231 if (!nstyle.length) {
1232 node.removeAttribute('style');
1235 this.iterateChildren(node, this.cleanWord);
1241 * iterateChildren of a Node, calling fn each time, using this as the scole..
1242 * @param {DomNode} node node to iterate children of.
1243 * @param {Function} fn method of this class to call on each item.
1245 iterateChildren : function(node, fn)
1247 if (!node.childNodes.length) {
1250 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1251 fn.call(this, node.childNodes[i])
1259 * Quite often pasting from word etc.. results in tables with column and widths.
1260 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1263 cleanTableWidths : function(node)
1268 this.cleanTableWidths(this.doc.body);
1273 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1276 Roo.log(node.tagName);
1277 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1278 this.iterateChildren(node, this.cleanTableWidths);
1281 if (node.hasAttribute('width')) {
1282 node.removeAttribute('width');
1286 if (node.hasAttribute("style")) {
1289 var styles = node.getAttribute("style").split(";");
1291 Roo.each(styles, function(s) {
1292 if (!s.match(/:/)) {
1295 var kv = s.split(":");
1296 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1299 // what ever is left... we allow.
1302 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1303 if (!nstyle.length) {
1304 node.removeAttribute('style');
1308 this.iterateChildren(node, this.cleanTableWidths);
1316 domToHTML : function(currentElement, depth, nopadtext) {
1319 nopadtext = nopadtext || false;
1321 if (!currentElement) {
1322 return this.domToHTML(this.doc.body);
1325 //Roo.log(currentElement);
1327 var allText = false;
1328 var nodeName = currentElement.nodeName;
1329 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1331 if (nodeName == '#text') {
1333 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1338 if (nodeName != 'BODY') {
1341 // Prints the node tagName, such as <A>, <IMG>, etc
1344 for(i = 0; i < currentElement.attributes.length;i++) {
1346 var aname = currentElement.attributes.item(i).name;
1347 if (!currentElement.attributes.item(i).value.length) {
1350 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1353 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1362 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1365 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1370 // Traverse the tree
1372 var currentElementChild = currentElement.childNodes.item(i);
1376 while (currentElementChild) {
1377 // Formatting code (indent the tree so it looks nice on the screen)
1378 var nopad = nopadtext;
1379 if (lastnode == 'SPAN') {
1383 if (currentElementChild.nodeName == '#text') {
1384 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1385 toadd = nopadtext ? toadd : toadd.trim();
1386 if (!nopad && toadd.length > 80) {
1387 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1392 currentElementChild = currentElement.childNodes.item(i);
1398 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1400 // Recursively traverse the tree structure of the child node
1401 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1402 lastnode = currentElementChild.nodeName;
1404 currentElementChild=currentElement.childNodes.item(i);
1410 // The remaining code is mostly for formatting the tree
1411 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1416 ret+= "</"+tagName+">";
1422 applyBlacklists : function()
1424 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1425 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1429 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1430 if (b.indexOf(tag) > -1) {
1433 this.white.push(tag);
1437 Roo.each(w, function(tag) {
1438 if (b.indexOf(tag) > -1) {
1441 if (this.white.indexOf(tag) > -1) {
1444 this.white.push(tag);
1449 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1450 if (w.indexOf(tag) > -1) {
1453 this.black.push(tag);
1457 Roo.each(b, function(tag) {
1458 if (w.indexOf(tag) > -1) {
1461 if (this.black.indexOf(tag) > -1) {
1464 this.black.push(tag);
1469 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1470 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1474 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1475 if (b.indexOf(tag) > -1) {
1478 this.cwhite.push(tag);
1482 Roo.each(w, function(tag) {
1483 if (b.indexOf(tag) > -1) {
1486 if (this.cwhite.indexOf(tag) > -1) {
1489 this.cwhite.push(tag);
1494 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1495 if (w.indexOf(tag) > -1) {
1498 this.cblack.push(tag);
1502 Roo.each(b, function(tag) {
1503 if (w.indexOf(tag) > -1) {
1506 if (this.cblack.indexOf(tag) > -1) {
1509 this.cblack.push(tag);
1514 setStylesheets : function(stylesheets)
1516 if(typeof(stylesheets) == 'string'){
1517 Roo.get(this.iframe.contentDocument.head).createChild({
1528 Roo.each(stylesheets, function(s) {
1533 Roo.get(_this.iframe.contentDocument.head).createChild({
1544 removeStylesheets : function()
1548 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1553 // hide stuff that is not compatible
1571 * @cfg {String} fieldClass @hide
1574 * @cfg {String} focusClass @hide
1577 * @cfg {String} autoCreate @hide
1580 * @cfg {String} inputType @hide
1583 * @cfg {String} invalidClass @hide
1586 * @cfg {String} invalidText @hide
1589 * @cfg {String} msgFx @hide
1592 * @cfg {String} validateOnBlur @hide
1596 Roo.HtmlEditorCore.white = [
1597 'area', 'br', 'img', 'input', 'hr', 'wbr',
1599 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1600 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1601 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1602 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1603 'table', 'ul', 'xmp',
1605 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1608 'dir', 'menu', 'ol', 'ul', 'dl',
1614 Roo.HtmlEditorCore.black = [
1615 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1617 'base', 'basefont', 'bgsound', 'blink', 'body',
1618 'frame', 'frameset', 'head', 'html', 'ilayer',
1619 'iframe', 'layer', 'link', 'meta', 'object',
1620 'script', 'style' ,'title', 'xml' // clean later..
1622 Roo.HtmlEditorCore.clean = [
1623 'script', 'style', 'title', 'xml'
1625 Roo.HtmlEditorCore.remove = [
1630 Roo.HtmlEditorCore.ablack = [
1634 Roo.HtmlEditorCore.aclean = [
1635 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1639 Roo.HtmlEditorCore.pwhite= [
1640 'http', 'https', 'mailto'
1643 // white listed style attributes.
1644 Roo.HtmlEditorCore.cwhite= [
1645 // 'text-align', /// default is to allow most things..
1651 // black listed style attributes.
1652 Roo.HtmlEditorCore.cblack= [
1653 // 'font-size' -- this can be set by the project
1657 Roo.HtmlEditorCore.swapCodes =[