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;}' +
164 st = '<style type="text/css">' +
169 st += '<style type="text/css">' +
170 'IMG { cursor: pointer } ' +
173 var cls = 'roo-htmleditor-body';
175 if(this.bodyCls.length){
176 cls += ' ' + this.bodyCls;
179 return '<html><head>' + st +
180 //<style type="text/css">' +
181 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
183 ' </head><body class="' + cls + '"></body></html>';
187 onRender : function(ct, position)
190 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
191 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
194 this.el.dom.style.border = '0 none';
195 this.el.dom.setAttribute('tabIndex', -1);
196 this.el.addClass('x-hidden hide');
200 if(Roo.isIE){ // fix IE 1px bogus margin
201 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
205 this.frameId = Roo.id();
209 var iframe = this.owner.wrap.createChild({
211 cls: 'form-control', // bootstrap..
215 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
220 this.iframe = iframe.dom;
224 this.doc.designMode = 'on';
227 this.doc.write(this.getDocMarkup());
231 var task = { // must defer to wait for browser to be ready
233 //console.log("run task?" + this.doc.readyState);
235 if(this.doc.body || this.doc.readyState == 'complete'){
237 this.doc.designMode="on";
241 Roo.TaskMgr.stop(task);
242 this.initEditor.defer(10, this);
249 Roo.TaskMgr.start(task);
254 onResize : function(w, h)
256 Roo.log('resize: ' +w + ',' + h );
257 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
261 if(typeof w == 'number'){
263 this.iframe.style.width = w + 'px';
265 if(typeof h == 'number'){
267 this.iframe.style.height = h + 'px';
269 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
276 * Toggles the editor between standard and source edit mode.
277 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
279 toggleSourceEdit : function(sourceEditMode){
281 this.sourceEditMode = sourceEditMode === true;
283 if(this.sourceEditMode){
285 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
288 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
289 //this.iframe.className = '';
292 //this.setSize(this.owner.wrap.getSize());
293 //this.fireEvent('editmodechange', this, this.sourceEditMode);
300 * Protected method that will not generally be called directly. If you need/want
301 * custom HTML cleanup, this is the method you should override.
302 * @param {String} html The HTML to be cleaned
303 * return {String} The cleaned HTML
305 cleanHtml : function(html){
308 if(Roo.isSafari){ // strip safari nonsense
309 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
312 if(html == ' '){
319 * HTML Editor -> Textarea
320 * Protected method that will not generally be called directly. Syncs the contents
321 * of the editor iframe with the textarea.
323 syncValue : function(){
324 if(this.initialized){
325 var bd = (this.doc.body || this.doc.documentElement);
326 //this.cleanUpPaste(); -- this is done else where and causes havoc..
327 var html = bd.innerHTML;
329 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
330 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
332 html = '<div style="'+m[0]+'">' + html + '</div>';
335 html = this.cleanHtml(html);
336 // fix up the special chars.. normaly like back quotes in word...
337 // however we do not want to do this with chinese..
338 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
339 var high, low, charValue, rep
340 var cc = match.charCodeAt();
342 // Get the character value, handling surrogate pairs
343 if (match.length == 2) {
344 // It's a surrogate pair, calculate the Unicode code point
345 high = match.charCodeAt(0) - 0xD800;
346 low = match.charCodeAt(1) - 0xDC00;
347 charValue = (high * 0x400) + low + 0x10000;
349 (cc >= 0x4E00 && cc < 0xA000 ) ||
350 (cc >= 0x3400 && cc < 0x4E00 ) ||
351 (cc >= 0xf900 && cc < 0xfb00 )
355 // Not a surrogate pair, the value *is* the Unicode code point
356 charValue = match.charCodeAt(0);
359 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
360 return "&#" + charValue + ";";
367 if(this.owner.fireEvent('beforesync', this, html) !== false){
368 this.el.dom.value = html;
369 this.owner.fireEvent('sync', this, html);
375 * Protected method that will not generally be called directly. Pushes the value of the textarea
376 * into the iframe editor.
378 pushValue : function(){
379 if(this.initialized){
380 var v = this.el.dom.value.trim();
386 if(this.owner.fireEvent('beforepush', this, v) !== false){
387 var d = (this.doc.body || this.doc.documentElement);
390 this.el.dom.value = d.innerHTML;
391 this.owner.fireEvent('push', this, v);
397 deferFocus : function(){
398 this.focus.defer(10, this);
403 if(this.win && !this.sourceEditMode){
410 assignDocWin: function()
412 var iframe = this.iframe;
415 this.doc = iframe.contentWindow.document;
416 this.win = iframe.contentWindow;
418 // if (!Roo.get(this.frameId)) {
421 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
422 // this.win = Roo.get(this.frameId).dom.contentWindow;
424 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
428 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
429 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
434 initEditor : function(){
435 //console.log("INIT EDITOR");
440 this.doc.designMode="on";
442 this.doc.write(this.getDocMarkup());
445 var dbody = (this.doc.body || this.doc.documentElement);
446 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
447 // this copies styles from the containing element into thsi one..
448 // not sure why we need all of this..
449 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
451 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
452 //ss['background-attachment'] = 'fixed'; // w3c
453 dbody.bgProperties = 'fixed'; // ie
454 //Roo.DomHelper.applyStyles(dbody, ss);
455 Roo.EventManager.on(this.doc, {
456 //'mousedown': this.onEditorEvent,
457 'mouseup': this.onEditorEvent,
458 'dblclick': this.onEditorEvent,
459 'click': this.onEditorEvent,
460 'keyup': this.onEditorEvent,
465 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
467 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
468 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
470 this.initialized = true;
472 this.owner.fireEvent('initialize', this);
477 onDestroy : function(){
483 //for (var i =0; i < this.toolbars.length;i++) {
484 // // fixme - ask toolbars for heights?
485 // this.toolbars[i].onDestroy();
488 //this.wrap.dom.innerHTML = '';
489 //this.wrap.remove();
494 onFirstFocus : function(){
499 this.activated = true;
502 if(Roo.isGecko){ // prevent silly gecko errors
504 var s = this.win.getSelection();
505 if(!s.focusNode || s.focusNode.nodeType != 3){
506 var r = s.getRangeAt(0);
507 r.selectNodeContents((this.doc.body || this.doc.documentElement));
512 this.execCmd('useCSS', true);
513 this.execCmd('styleWithCSS', false);
516 this.owner.fireEvent('activate', this);
520 adjustFont: function(btn){
521 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
522 //if(Roo.isSafari){ // safari
525 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
526 if(Roo.isSafari){ // safari
527 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
528 v = (v < 10) ? 10 : v;
529 v = (v > 48) ? 48 : v;
530 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
535 v = Math.max(1, v+adjust);
537 this.execCmd('FontSize', v );
540 onEditorEvent : function(e)
542 this.owner.fireEvent('editorevent', this, e);
543 // this.updateToolbar();
544 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
547 insertTag : function(tg)
549 // could be a bit smarter... -> wrap the current selected tRoo..
550 if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
552 range = this.createRange(this.getSelection());
553 var wrappingNode = this.doc.createElement(tg.toLowerCase());
554 wrappingNode.appendChild(range.extractContents());
555 range.insertNode(wrappingNode);
562 this.execCmd("formatblock", tg);
566 insertText : function(txt)
570 var range = this.createRange();
571 range.deleteContents();
572 //alert(Sender.getAttribute('label'));
574 range.insertNode(this.doc.createTextNode(txt));
580 * Executes a Midas editor command on the editor document and performs necessary focus and
581 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
582 * @param {String} cmd The Midas command
583 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
585 relayCmd : function(cmd, value){
587 this.execCmd(cmd, value);
588 this.owner.fireEvent('editorevent', this);
589 //this.updateToolbar();
590 this.owner.deferFocus();
594 * Executes a Midas editor command directly on the editor document.
595 * For visual commands, you should use {@link #relayCmd} instead.
596 * <b>This should only be called after the editor is initialized.</b>
597 * @param {String} cmd The Midas command
598 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
600 execCmd : function(cmd, value){
601 this.doc.execCommand(cmd, false, value === undefined ? null : value);
608 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
610 * @param {String} text | dom node..
612 insertAtCursor : function(text)
621 var r = this.doc.selection.createRange();
632 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
636 // from jquery ui (MIT licenced)
640 if (win.getSelection && win.getSelection().getRangeAt) {
641 range = win.getSelection().getRangeAt(0);
642 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
643 range.insertNode(node);
644 } else if (win.document.selection && win.document.selection.createRange) {
645 // no firefox support
646 var txt = typeof(text) == 'string' ? text : text.outerHTML;
647 win.document.selection.createRange().pasteHTML(txt);
649 // no firefox support
650 var txt = typeof(text) == 'string' ? text : text.outerHTML;
651 this.execCmd('InsertHTML', txt);
660 mozKeyPress : function(e){
662 var c = e.getCharCode(), cmd;
665 c = String.fromCharCode(c).toLowerCase();
679 this.cleanUpPaste.defer(100, this);
695 fixKeys : function(){ // load time branching for fastest keydown performance
698 var k = e.getKey(), r;
701 r = this.doc.selection.createRange();
704 r.pasteHTML('    ');
711 r = this.doc.selection.createRange();
713 var target = r.parentElement();
714 if(!target || target.tagName.toLowerCase() != 'li'){
716 r.pasteHTML('<br />');
722 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
723 this.cleanUpPaste.defer(100, this);
729 }else if(Roo.isOpera){
735 this.execCmd('InsertHTML','    ');
738 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
739 this.cleanUpPaste.defer(100, this);
744 }else if(Roo.isSafari){
750 this.execCmd('InsertText','\t');
754 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
755 this.cleanUpPaste.defer(100, this);
763 getAllAncestors: function()
765 var p = this.getSelectedNode();
768 a.push(p); // push blank onto stack..
769 p = this.getParentElement();
773 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
777 a.push(this.doc.body);
784 getSelection : function()
787 return Roo.isIE ? this.doc.selection : this.win.getSelection();
790 getSelectedNode: function()
792 // this may only work on Gecko!!!
794 // should we cache this!!!!
799 var range = this.createRange(this.getSelection()).cloneRange();
802 var parent = range.parentElement();
804 var testRange = range.duplicate();
805 testRange.moveToElementText(parent);
806 if (testRange.inRange(range)) {
809 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
812 parent = parent.parentElement;
817 // is ancestor a text element.
818 var ac = range.commonAncestorContainer;
819 if (ac.nodeType == 3) {
823 var ar = ac.childNodes;
826 var other_nodes = [];
827 var has_other_nodes = false;
828 for (var i=0;i<ar.length;i++) {
829 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
832 // fullly contained node.
834 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
839 // probably selected..
840 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
841 other_nodes.push(ar[i]);
845 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
850 has_other_nodes = true;
852 if (!nodes.length && other_nodes.length) {
855 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
861 createRange: function(sel)
863 // this has strange effects when using with
864 // top toolbar - not sure if it's a great idea.
865 //this.editor.contentWindow.focus();
866 if (typeof sel != "undefined") {
868 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
870 return this.doc.createRange();
873 return this.doc.createRange();
876 getParentElement: function()
880 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
882 var range = this.createRange(sel);
885 var p = range.commonAncestorContainer;
886 while (p.nodeType == 3) { // text node
897 * Range intersection.. the hard stuff...
901 * [ -- selected range --- ]
905 * if end is before start or hits it. fail.
906 * if start is after end or hits it fail.
908 * if either hits (but other is outside. - then it's not
914 // @see http://www.thismuchiknow.co.uk/?p=64.
915 rangeIntersectsNode : function(range, node)
917 var nodeRange = node.ownerDocument.createRange();
919 nodeRange.selectNode(node);
921 nodeRange.selectNodeContents(node);
924 var rangeStartRange = range.cloneRange();
925 rangeStartRange.collapse(true);
927 var rangeEndRange = range.cloneRange();
928 rangeEndRange.collapse(false);
930 var nodeStartRange = nodeRange.cloneRange();
931 nodeStartRange.collapse(true);
933 var nodeEndRange = nodeRange.cloneRange();
934 nodeEndRange.collapse(false);
936 return rangeStartRange.compareBoundaryPoints(
937 Range.START_TO_START, nodeEndRange) == -1 &&
938 rangeEndRange.compareBoundaryPoints(
939 Range.START_TO_START, nodeStartRange) == 1;
943 rangeCompareNode : function(range, node)
945 var nodeRange = node.ownerDocument.createRange();
947 nodeRange.selectNode(node);
949 nodeRange.selectNodeContents(node);
953 range.collapse(true);
955 nodeRange.collapse(true);
957 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
958 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
960 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
962 var nodeIsBefore = ss == 1;
963 var nodeIsAfter = ee == -1;
965 if (nodeIsBefore && nodeIsAfter) {
968 if (!nodeIsBefore && nodeIsAfter) {
969 return 1; //right trailed.
972 if (nodeIsBefore && !nodeIsAfter) {
973 return 2; // left trailed.
979 // private? - in a new class?
980 cleanUpPaste : function()
982 // cleans up the whole document..
983 Roo.log('cleanuppaste');
985 this.cleanUpChildren(this.doc.body);
986 var clean = this.cleanWordChars(this.doc.body.innerHTML);
987 if (clean != this.doc.body.innerHTML) {
988 this.doc.body.innerHTML = clean;
993 cleanWordChars : function(input) {// change the chars to hex code
994 var he = Roo.HtmlEditorCore;
997 Roo.each(he.swapCodes, function(sw) {
998 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1000 output = output.replace(swapper, sw[1]);
1007 cleanUpChildren : function (n)
1009 if (!n.childNodes.length) {
1012 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1013 this.cleanUpChild(n.childNodes[i]);
1020 cleanUpChild : function (node)
1023 //console.log(node);
1024 if (node.nodeName == "#text") {
1025 // clean up silly Windows -- stuff?
1028 if (node.nodeName == "#comment") {
1029 node.parentNode.removeChild(node);
1030 // clean up silly Windows -- stuff?
1033 var lcname = node.tagName.toLowerCase();
1034 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1035 // whitelist of tags..
1037 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1039 node.parentNode.removeChild(node);
1044 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1046 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1047 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1049 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1050 // remove_keep_children = true;
1053 if (remove_keep_children) {
1054 this.cleanUpChildren(node);
1055 // inserts everything just before this node...
1056 while (node.childNodes.length) {
1057 var cn = node.childNodes[0];
1058 node.removeChild(cn);
1059 node.parentNode.insertBefore(cn, node);
1061 node.parentNode.removeChild(node);
1065 if (!node.attributes || !node.attributes.length) {
1066 this.cleanUpChildren(node);
1070 function cleanAttr(n,v)
1073 if (v.match(/^\./) || v.match(/^\//)) {
1076 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1079 if (v.match(/^#/)) {
1082 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1083 node.removeAttribute(n);
1087 var cwhite = this.cwhite;
1088 var cblack = this.cblack;
1090 function cleanStyle(n,v)
1092 if (v.match(/expression/)) { //XSS?? should we even bother..
1093 node.removeAttribute(n);
1097 var parts = v.split(/;/);
1100 Roo.each(parts, function(p) {
1101 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1105 var l = p.split(':').shift().replace(/\s+/g,'');
1106 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1108 if ( cwhite.length && cblack.indexOf(l) > -1) {
1109 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1110 //node.removeAttribute(n);
1114 // only allow 'c whitelisted system attributes'
1115 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1116 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1117 //node.removeAttribute(n);
1128 node.setAttribute(n, clean.join(';'));
1130 node.removeAttribute(n);
1136 for (var i = node.attributes.length-1; i > -1 ; i--) {
1137 var a = node.attributes[i];
1140 if (a.name.toLowerCase().substr(0,2)=='on') {
1141 node.removeAttribute(a.name);
1144 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1145 node.removeAttribute(a.name);
1148 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1149 cleanAttr(a.name,a.value); // fixme..
1152 if (a.name == 'style') {
1153 cleanStyle(a.name,a.value);
1156 /// clean up MS crap..
1157 // tecnically this should be a list of valid class'es..
1160 if (a.name == 'class') {
1161 if (a.value.match(/^Mso/)) {
1162 node.className = '';
1165 if (a.value.match(/^body$/)) {
1166 node.className = '';
1177 this.cleanUpChildren(node);
1183 * Clean up MS wordisms...
1185 cleanWord : function(node)
1188 this.cleanWord(this.doc.body);
1193 node.nodeName == 'SPAN' &&
1194 !node.hasAttributes() &&
1195 node.childNodes.length == 1 &&
1196 node.firstChild.nodeName == "#text"
1198 var textNode = node.firstChild;
1199 node.removeChild(textNode);
1200 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1201 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1203 node.parentNode.insertBefore(textNode, node);
1204 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1205 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1207 node.parentNode.removeChild(node);
1210 if (node.nodeName == "#text") {
1211 // clean up silly Windows -- stuff?
1214 if (node.nodeName == "#comment") {
1215 node.parentNode.removeChild(node);
1216 // clean up silly Windows -- stuff?
1220 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1221 node.parentNode.removeChild(node);
1225 // remove - but keep children..
1226 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1227 while (node.childNodes.length) {
1228 var cn = node.childNodes[0];
1229 node.removeChild(cn);
1230 node.parentNode.insertBefore(cn, node);
1232 node.parentNode.removeChild(node);
1233 this.iterateChildren(node, this.cleanWord);
1237 if (node.className.length) {
1239 var cn = node.className.split(/\W+/);
1241 Roo.each(cn, function(cls) {
1242 if (cls.match(/Mso[a-zA-Z]+/)) {
1247 node.className = cna.length ? cna.join(' ') : '';
1249 node.removeAttribute("class");
1253 if (node.hasAttribute("lang")) {
1254 node.removeAttribute("lang");
1257 if (node.hasAttribute("style")) {
1259 var styles = node.getAttribute("style").split(";");
1261 Roo.each(styles, function(s) {
1262 if (!s.match(/:/)) {
1265 var kv = s.split(":");
1266 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1269 // what ever is left... we allow.
1272 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1273 if (!nstyle.length) {
1274 node.removeAttribute('style');
1277 this.iterateChildren(node, this.cleanWord);
1283 * iterateChildren of a Node, calling fn each time, using this as the scole..
1284 * @param {DomNode} node node to iterate children of.
1285 * @param {Function} fn method of this class to call on each item.
1287 iterateChildren : function(node, fn)
1289 if (!node.childNodes.length) {
1292 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1293 fn.call(this, node.childNodes[i])
1301 * Quite often pasting from word etc.. results in tables with column and widths.
1302 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1305 cleanTableWidths : function(node)
1310 this.cleanTableWidths(this.doc.body);
1315 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1318 Roo.log(node.tagName);
1319 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1320 this.iterateChildren(node, this.cleanTableWidths);
1323 if (node.hasAttribute('width')) {
1324 node.removeAttribute('width');
1328 if (node.hasAttribute("style")) {
1331 var styles = node.getAttribute("style").split(";");
1333 Roo.each(styles, function(s) {
1334 if (!s.match(/:/)) {
1337 var kv = s.split(":");
1338 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1341 // what ever is left... we allow.
1344 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1345 if (!nstyle.length) {
1346 node.removeAttribute('style');
1350 this.iterateChildren(node, this.cleanTableWidths);
1358 domToHTML : function(currentElement, depth, nopadtext) {
1361 nopadtext = nopadtext || false;
1363 if (!currentElement) {
1364 return this.domToHTML(this.doc.body);
1367 //Roo.log(currentElement);
1369 var allText = false;
1370 var nodeName = currentElement.nodeName;
1371 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1373 if (nodeName == '#text') {
1375 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1380 if (nodeName != 'BODY') {
1383 // Prints the node tagName, such as <A>, <IMG>, etc
1386 for(i = 0; i < currentElement.attributes.length;i++) {
1388 var aname = currentElement.attributes.item(i).name;
1389 if (!currentElement.attributes.item(i).value.length) {
1392 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1395 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1404 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1407 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1412 // Traverse the tree
1414 var currentElementChild = currentElement.childNodes.item(i);
1418 while (currentElementChild) {
1419 // Formatting code (indent the tree so it looks nice on the screen)
1420 var nopad = nopadtext;
1421 if (lastnode == 'SPAN') {
1425 if (currentElementChild.nodeName == '#text') {
1426 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1427 toadd = nopadtext ? toadd : toadd.trim();
1428 if (!nopad && toadd.length > 80) {
1429 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1434 currentElementChild = currentElement.childNodes.item(i);
1440 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1442 // Recursively traverse the tree structure of the child node
1443 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1444 lastnode = currentElementChild.nodeName;
1446 currentElementChild=currentElement.childNodes.item(i);
1452 // The remaining code is mostly for formatting the tree
1453 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1458 ret+= "</"+tagName+">";
1464 applyBlacklists : function()
1466 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1467 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1471 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1472 if (b.indexOf(tag) > -1) {
1475 this.white.push(tag);
1479 Roo.each(w, function(tag) {
1480 if (b.indexOf(tag) > -1) {
1483 if (this.white.indexOf(tag) > -1) {
1486 this.white.push(tag);
1491 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1492 if (w.indexOf(tag) > -1) {
1495 this.black.push(tag);
1499 Roo.each(b, function(tag) {
1500 if (w.indexOf(tag) > -1) {
1503 if (this.black.indexOf(tag) > -1) {
1506 this.black.push(tag);
1511 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1512 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1516 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1517 if (b.indexOf(tag) > -1) {
1520 this.cwhite.push(tag);
1524 Roo.each(w, function(tag) {
1525 if (b.indexOf(tag) > -1) {
1528 if (this.cwhite.indexOf(tag) > -1) {
1531 this.cwhite.push(tag);
1536 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1537 if (w.indexOf(tag) > -1) {
1540 this.cblack.push(tag);
1544 Roo.each(b, function(tag) {
1545 if (w.indexOf(tag) > -1) {
1548 if (this.cblack.indexOf(tag) > -1) {
1551 this.cblack.push(tag);
1556 setStylesheets : function(stylesheets)
1558 if(typeof(stylesheets) == 'string'){
1559 Roo.get(this.iframe.contentDocument.head).createChild({
1570 Roo.each(stylesheets, function(s) {
1575 Roo.get(_this.iframe.contentDocument.head).createChild({
1586 removeStylesheets : function()
1590 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1595 setStyle : function(style)
1597 Roo.get(this.iframe.contentDocument.head).createChild({
1606 // hide stuff that is not compatible
1624 * @cfg {String} fieldClass @hide
1627 * @cfg {String} focusClass @hide
1630 * @cfg {String} autoCreate @hide
1633 * @cfg {String} inputType @hide
1636 * @cfg {String} invalidClass @hide
1639 * @cfg {String} invalidText @hide
1642 * @cfg {String} msgFx @hide
1645 * @cfg {String} validateOnBlur @hide
1649 Roo.HtmlEditorCore.white = [
1650 'area', 'br', 'img', 'input', 'hr', 'wbr',
1652 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1653 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1654 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1655 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1656 'table', 'ul', 'xmp',
1658 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1661 'dir', 'menu', 'ol', 'ul', 'dl',
1667 Roo.HtmlEditorCore.black = [
1668 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1670 'base', 'basefont', 'bgsound', 'blink', 'body',
1671 'frame', 'frameset', 'head', 'html', 'ilayer',
1672 'iframe', 'layer', 'link', 'meta', 'object',
1673 'script', 'style' ,'title', 'xml' // clean later..
1675 Roo.HtmlEditorCore.clean = [
1676 'script', 'style', 'title', 'xml'
1678 Roo.HtmlEditorCore.remove = [
1683 Roo.HtmlEditorCore.ablack = [
1687 Roo.HtmlEditorCore.aclean = [
1688 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1692 Roo.HtmlEditorCore.pwhite= [
1693 'http', 'https', 'mailto'
1696 // white listed style attributes.
1697 Roo.HtmlEditorCore.cwhite= [
1698 // 'text-align', /// default is to allow most things..
1704 // black listed style attributes.
1705 Roo.HtmlEditorCore.cblack= [
1706 // 'font-size' -- this can be set by the project
1710 Roo.HtmlEditorCore.swapCodes =[