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 // See if we have a mapping for it
361 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
362 var rep = "&#" + charValue + ";";
364 // Return replacement
370 if(this.owner.fireEvent('beforesync', this, html) !== false){
371 this.el.dom.value = html;
372 this.owner.fireEvent('sync', this, html);
378 * Protected method that will not generally be called directly. Pushes the value of the textarea
379 * into the iframe editor.
381 pushValue : function(){
382 if(this.initialized){
383 var v = this.el.dom.value.trim();
389 if(this.owner.fireEvent('beforepush', this, v) !== false){
390 var d = (this.doc.body || this.doc.documentElement);
393 this.el.dom.value = d.innerHTML;
394 this.owner.fireEvent('push', this, v);
400 deferFocus : function(){
401 this.focus.defer(10, this);
406 if(this.win && !this.sourceEditMode){
413 assignDocWin: function()
415 var iframe = this.iframe;
418 this.doc = iframe.contentWindow.document;
419 this.win = iframe.contentWindow;
421 // if (!Roo.get(this.frameId)) {
424 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
425 // this.win = Roo.get(this.frameId).dom.contentWindow;
427 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
431 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
432 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
437 initEditor : function(){
438 //console.log("INIT EDITOR");
443 this.doc.designMode="on";
445 this.doc.write(this.getDocMarkup());
448 var dbody = (this.doc.body || this.doc.documentElement);
449 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
450 // this copies styles from the containing element into thsi one..
451 // not sure why we need all of this..
452 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
454 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
455 //ss['background-attachment'] = 'fixed'; // w3c
456 dbody.bgProperties = 'fixed'; // ie
457 //Roo.DomHelper.applyStyles(dbody, ss);
458 Roo.EventManager.on(this.doc, {
459 //'mousedown': this.onEditorEvent,
460 'mouseup': this.onEditorEvent,
461 'dblclick': this.onEditorEvent,
462 'click': this.onEditorEvent,
463 'keyup': this.onEditorEvent,
468 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
470 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
471 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
473 this.initialized = true;
475 this.owner.fireEvent('initialize', this);
480 onDestroy : function(){
486 //for (var i =0; i < this.toolbars.length;i++) {
487 // // fixme - ask toolbars for heights?
488 // this.toolbars[i].onDestroy();
491 //this.wrap.dom.innerHTML = '';
492 //this.wrap.remove();
497 onFirstFocus : function(){
502 this.activated = true;
505 if(Roo.isGecko){ // prevent silly gecko errors
507 var s = this.win.getSelection();
508 if(!s.focusNode || s.focusNode.nodeType != 3){
509 var r = s.getRangeAt(0);
510 r.selectNodeContents((this.doc.body || this.doc.documentElement));
515 this.execCmd('useCSS', true);
516 this.execCmd('styleWithCSS', false);
519 this.owner.fireEvent('activate', this);
523 adjustFont: function(btn){
524 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
525 //if(Roo.isSafari){ // safari
528 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
529 if(Roo.isSafari){ // safari
530 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
531 v = (v < 10) ? 10 : v;
532 v = (v > 48) ? 48 : v;
533 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
538 v = Math.max(1, v+adjust);
540 this.execCmd('FontSize', v );
543 onEditorEvent : function(e)
545 this.owner.fireEvent('editorevent', this, e);
546 // this.updateToolbar();
547 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
550 insertTag : function(tg)
552 // could be a bit smarter... -> wrap the current selected tRoo..
553 if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
555 range = this.createRange(this.getSelection());
556 var wrappingNode = this.doc.createElement(tg.toLowerCase());
557 wrappingNode.appendChild(range.extractContents());
558 range.insertNode(wrappingNode);
565 this.execCmd("formatblock", tg);
569 insertText : function(txt)
573 var range = this.createRange();
574 range.deleteContents();
575 //alert(Sender.getAttribute('label'));
577 range.insertNode(this.doc.createTextNode(txt));
583 * Executes a Midas editor command on the editor document and performs necessary focus and
584 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
585 * @param {String} cmd The Midas command
586 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
588 relayCmd : function(cmd, value){
590 this.execCmd(cmd, value);
591 this.owner.fireEvent('editorevent', this);
592 //this.updateToolbar();
593 this.owner.deferFocus();
597 * Executes a Midas editor command directly on the editor document.
598 * For visual commands, you should use {@link #relayCmd} instead.
599 * <b>This should only be called after the editor is initialized.</b>
600 * @param {String} cmd The Midas command
601 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
603 execCmd : function(cmd, value){
604 this.doc.execCommand(cmd, false, value === undefined ? null : value);
611 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
613 * @param {String} text | dom node..
615 insertAtCursor : function(text)
624 var r = this.doc.selection.createRange();
635 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
639 // from jquery ui (MIT licenced)
643 if (win.getSelection && win.getSelection().getRangeAt) {
644 range = win.getSelection().getRangeAt(0);
645 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
646 range.insertNode(node);
647 } else if (win.document.selection && win.document.selection.createRange) {
648 // no firefox support
649 var txt = typeof(text) == 'string' ? text : text.outerHTML;
650 win.document.selection.createRange().pasteHTML(txt);
652 // no firefox support
653 var txt = typeof(text) == 'string' ? text : text.outerHTML;
654 this.execCmd('InsertHTML', txt);
663 mozKeyPress : function(e){
665 var c = e.getCharCode(), cmd;
668 c = String.fromCharCode(c).toLowerCase();
682 this.cleanUpPaste.defer(100, this);
698 fixKeys : function(){ // load time branching for fastest keydown performance
701 var k = e.getKey(), r;
704 r = this.doc.selection.createRange();
707 r.pasteHTML('    ');
714 r = this.doc.selection.createRange();
716 var target = r.parentElement();
717 if(!target || target.tagName.toLowerCase() != 'li'){
719 r.pasteHTML('<br />');
725 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
726 this.cleanUpPaste.defer(100, this);
732 }else if(Roo.isOpera){
738 this.execCmd('InsertHTML','    ');
741 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
742 this.cleanUpPaste.defer(100, this);
747 }else if(Roo.isSafari){
753 this.execCmd('InsertText','\t');
757 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
758 this.cleanUpPaste.defer(100, this);
766 getAllAncestors: function()
768 var p = this.getSelectedNode();
771 a.push(p); // push blank onto stack..
772 p = this.getParentElement();
776 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
780 a.push(this.doc.body);
787 getSelection : function()
790 return Roo.isIE ? this.doc.selection : this.win.getSelection();
793 getSelectedNode: function()
795 // this may only work on Gecko!!!
797 // should we cache this!!!!
802 var range = this.createRange(this.getSelection()).cloneRange();
805 var parent = range.parentElement();
807 var testRange = range.duplicate();
808 testRange.moveToElementText(parent);
809 if (testRange.inRange(range)) {
812 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
815 parent = parent.parentElement;
820 // is ancestor a text element.
821 var ac = range.commonAncestorContainer;
822 if (ac.nodeType == 3) {
826 var ar = ac.childNodes;
829 var other_nodes = [];
830 var has_other_nodes = false;
831 for (var i=0;i<ar.length;i++) {
832 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
835 // fullly contained node.
837 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
842 // probably selected..
843 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
844 other_nodes.push(ar[i]);
848 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
853 has_other_nodes = true;
855 if (!nodes.length && other_nodes.length) {
858 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
864 createRange: function(sel)
866 // this has strange effects when using with
867 // top toolbar - not sure if it's a great idea.
868 //this.editor.contentWindow.focus();
869 if (typeof sel != "undefined") {
871 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
873 return this.doc.createRange();
876 return this.doc.createRange();
879 getParentElement: function()
883 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
885 var range = this.createRange(sel);
888 var p = range.commonAncestorContainer;
889 while (p.nodeType == 3) { // text node
900 * Range intersection.. the hard stuff...
904 * [ -- selected range --- ]
908 * if end is before start or hits it. fail.
909 * if start is after end or hits it fail.
911 * if either hits (but other is outside. - then it's not
917 // @see http://www.thismuchiknow.co.uk/?p=64.
918 rangeIntersectsNode : function(range, node)
920 var nodeRange = node.ownerDocument.createRange();
922 nodeRange.selectNode(node);
924 nodeRange.selectNodeContents(node);
927 var rangeStartRange = range.cloneRange();
928 rangeStartRange.collapse(true);
930 var rangeEndRange = range.cloneRange();
931 rangeEndRange.collapse(false);
933 var nodeStartRange = nodeRange.cloneRange();
934 nodeStartRange.collapse(true);
936 var nodeEndRange = nodeRange.cloneRange();
937 nodeEndRange.collapse(false);
939 return rangeStartRange.compareBoundaryPoints(
940 Range.START_TO_START, nodeEndRange) == -1 &&
941 rangeEndRange.compareBoundaryPoints(
942 Range.START_TO_START, nodeStartRange) == 1;
946 rangeCompareNode : function(range, node)
948 var nodeRange = node.ownerDocument.createRange();
950 nodeRange.selectNode(node);
952 nodeRange.selectNodeContents(node);
956 range.collapse(true);
958 nodeRange.collapse(true);
960 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
961 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
963 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
965 var nodeIsBefore = ss == 1;
966 var nodeIsAfter = ee == -1;
968 if (nodeIsBefore && nodeIsAfter) {
971 if (!nodeIsBefore && nodeIsAfter) {
972 return 1; //right trailed.
975 if (nodeIsBefore && !nodeIsAfter) {
976 return 2; // left trailed.
982 // private? - in a new class?
983 cleanUpPaste : function()
985 // cleans up the whole document..
986 Roo.log('cleanuppaste');
988 this.cleanUpChildren(this.doc.body);
989 var clean = this.cleanWordChars(this.doc.body.innerHTML);
990 if (clean != this.doc.body.innerHTML) {
991 this.doc.body.innerHTML = clean;
996 cleanWordChars : function(input) {// change the chars to hex code
997 var he = Roo.HtmlEditorCore;
1000 Roo.each(he.swapCodes, function(sw) {
1001 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1003 output = output.replace(swapper, sw[1]);
1010 cleanUpChildren : function (n)
1012 if (!n.childNodes.length) {
1015 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1016 this.cleanUpChild(n.childNodes[i]);
1023 cleanUpChild : function (node)
1026 //console.log(node);
1027 if (node.nodeName == "#text") {
1028 // clean up silly Windows -- stuff?
1031 if (node.nodeName == "#comment") {
1032 node.parentNode.removeChild(node);
1033 // clean up silly Windows -- stuff?
1036 var lcname = node.tagName.toLowerCase();
1037 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1038 // whitelist of tags..
1040 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1042 node.parentNode.removeChild(node);
1047 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1049 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1050 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1052 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1053 // remove_keep_children = true;
1056 if (remove_keep_children) {
1057 this.cleanUpChildren(node);
1058 // inserts everything just before this node...
1059 while (node.childNodes.length) {
1060 var cn = node.childNodes[0];
1061 node.removeChild(cn);
1062 node.parentNode.insertBefore(cn, node);
1064 node.parentNode.removeChild(node);
1068 if (!node.attributes || !node.attributes.length) {
1069 this.cleanUpChildren(node);
1073 function cleanAttr(n,v)
1076 if (v.match(/^\./) || v.match(/^\//)) {
1079 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1082 if (v.match(/^#/)) {
1085 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1086 node.removeAttribute(n);
1090 var cwhite = this.cwhite;
1091 var cblack = this.cblack;
1093 function cleanStyle(n,v)
1095 if (v.match(/expression/)) { //XSS?? should we even bother..
1096 node.removeAttribute(n);
1100 var parts = v.split(/;/);
1103 Roo.each(parts, function(p) {
1104 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1108 var l = p.split(':').shift().replace(/\s+/g,'');
1109 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1111 if ( cwhite.length && cblack.indexOf(l) > -1) {
1112 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1113 //node.removeAttribute(n);
1117 // only allow 'c whitelisted system attributes'
1118 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1119 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1120 //node.removeAttribute(n);
1131 node.setAttribute(n, clean.join(';'));
1133 node.removeAttribute(n);
1139 for (var i = node.attributes.length-1; i > -1 ; i--) {
1140 var a = node.attributes[i];
1143 if (a.name.toLowerCase().substr(0,2)=='on') {
1144 node.removeAttribute(a.name);
1147 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1148 node.removeAttribute(a.name);
1151 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1152 cleanAttr(a.name,a.value); // fixme..
1155 if (a.name == 'style') {
1156 cleanStyle(a.name,a.value);
1159 /// clean up MS crap..
1160 // tecnically this should be a list of valid class'es..
1163 if (a.name == 'class') {
1164 if (a.value.match(/^Mso/)) {
1165 node.className = '';
1168 if (a.value.match(/^body$/)) {
1169 node.className = '';
1180 this.cleanUpChildren(node);
1186 * Clean up MS wordisms...
1188 cleanWord : function(node)
1191 this.cleanWord(this.doc.body);
1196 node.nodeName == 'SPAN' &&
1197 !node.hasAttributes() &&
1198 node.childNodes.length == 1 &&
1199 node.firstChild.nodeName == "#text"
1201 var textNode = node.firstChild;
1202 node.removeChild(textNode);
1203 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1204 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1206 node.parentNode.insertBefore(textNode, node);
1207 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1208 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1210 node.parentNode.removeChild(node);
1213 if (node.nodeName == "#text") {
1214 // clean up silly Windows -- stuff?
1217 if (node.nodeName == "#comment") {
1218 node.parentNode.removeChild(node);
1219 // clean up silly Windows -- stuff?
1223 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1224 node.parentNode.removeChild(node);
1228 // remove - but keep children..
1229 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1230 while (node.childNodes.length) {
1231 var cn = node.childNodes[0];
1232 node.removeChild(cn);
1233 node.parentNode.insertBefore(cn, node);
1235 node.parentNode.removeChild(node);
1236 this.iterateChildren(node, this.cleanWord);
1240 if (node.className.length) {
1242 var cn = node.className.split(/\W+/);
1244 Roo.each(cn, function(cls) {
1245 if (cls.match(/Mso[a-zA-Z]+/)) {
1250 node.className = cna.length ? cna.join(' ') : '';
1252 node.removeAttribute("class");
1256 if (node.hasAttribute("lang")) {
1257 node.removeAttribute("lang");
1260 if (node.hasAttribute("style")) {
1262 var styles = node.getAttribute("style").split(";");
1264 Roo.each(styles, function(s) {
1265 if (!s.match(/:/)) {
1268 var kv = s.split(":");
1269 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1272 // what ever is left... we allow.
1275 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1276 if (!nstyle.length) {
1277 node.removeAttribute('style');
1280 this.iterateChildren(node, this.cleanWord);
1286 * iterateChildren of a Node, calling fn each time, using this as the scole..
1287 * @param {DomNode} node node to iterate children of.
1288 * @param {Function} fn method of this class to call on each item.
1290 iterateChildren : function(node, fn)
1292 if (!node.childNodes.length) {
1295 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1296 fn.call(this, node.childNodes[i])
1304 * Quite often pasting from word etc.. results in tables with column and widths.
1305 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1308 cleanTableWidths : function(node)
1313 this.cleanTableWidths(this.doc.body);
1318 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1321 Roo.log(node.tagName);
1322 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1323 this.iterateChildren(node, this.cleanTableWidths);
1326 if (node.hasAttribute('width')) {
1327 node.removeAttribute('width');
1331 if (node.hasAttribute("style")) {
1334 var styles = node.getAttribute("style").split(";");
1336 Roo.each(styles, function(s) {
1337 if (!s.match(/:/)) {
1340 var kv = s.split(":");
1341 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1344 // what ever is left... we allow.
1347 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1348 if (!nstyle.length) {
1349 node.removeAttribute('style');
1353 this.iterateChildren(node, this.cleanTableWidths);
1361 domToHTML : function(currentElement, depth, nopadtext) {
1364 nopadtext = nopadtext || false;
1366 if (!currentElement) {
1367 return this.domToHTML(this.doc.body);
1370 //Roo.log(currentElement);
1372 var allText = false;
1373 var nodeName = currentElement.nodeName;
1374 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1376 if (nodeName == '#text') {
1378 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1383 if (nodeName != 'BODY') {
1386 // Prints the node tagName, such as <A>, <IMG>, etc
1389 for(i = 0; i < currentElement.attributes.length;i++) {
1391 var aname = currentElement.attributes.item(i).name;
1392 if (!currentElement.attributes.item(i).value.length) {
1395 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1398 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1407 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1410 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1415 // Traverse the tree
1417 var currentElementChild = currentElement.childNodes.item(i);
1421 while (currentElementChild) {
1422 // Formatting code (indent the tree so it looks nice on the screen)
1423 var nopad = nopadtext;
1424 if (lastnode == 'SPAN') {
1428 if (currentElementChild.nodeName == '#text') {
1429 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1430 toadd = nopadtext ? toadd : toadd.trim();
1431 if (!nopad && toadd.length > 80) {
1432 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1437 currentElementChild = currentElement.childNodes.item(i);
1443 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1445 // Recursively traverse the tree structure of the child node
1446 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1447 lastnode = currentElementChild.nodeName;
1449 currentElementChild=currentElement.childNodes.item(i);
1455 // The remaining code is mostly for formatting the tree
1456 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1461 ret+= "</"+tagName+">";
1467 applyBlacklists : function()
1469 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1470 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1474 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1475 if (b.indexOf(tag) > -1) {
1478 this.white.push(tag);
1482 Roo.each(w, function(tag) {
1483 if (b.indexOf(tag) > -1) {
1486 if (this.white.indexOf(tag) > -1) {
1489 this.white.push(tag);
1494 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1495 if (w.indexOf(tag) > -1) {
1498 this.black.push(tag);
1502 Roo.each(b, function(tag) {
1503 if (w.indexOf(tag) > -1) {
1506 if (this.black.indexOf(tag) > -1) {
1509 this.black.push(tag);
1514 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1515 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1519 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1520 if (b.indexOf(tag) > -1) {
1523 this.cwhite.push(tag);
1527 Roo.each(w, function(tag) {
1528 if (b.indexOf(tag) > -1) {
1531 if (this.cwhite.indexOf(tag) > -1) {
1534 this.cwhite.push(tag);
1539 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1540 if (w.indexOf(tag) > -1) {
1543 this.cblack.push(tag);
1547 Roo.each(b, function(tag) {
1548 if (w.indexOf(tag) > -1) {
1551 if (this.cblack.indexOf(tag) > -1) {
1554 this.cblack.push(tag);
1559 setStylesheets : function(stylesheets)
1561 if(typeof(stylesheets) == 'string'){
1562 Roo.get(this.iframe.contentDocument.head).createChild({
1573 Roo.each(stylesheets, function(s) {
1578 Roo.get(_this.iframe.contentDocument.head).createChild({
1589 removeStylesheets : function()
1593 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1598 setStyle : function(style)
1600 Roo.get(this.iframe.contentDocument.head).createChild({
1609 // hide stuff that is not compatible
1627 * @cfg {String} fieldClass @hide
1630 * @cfg {String} focusClass @hide
1633 * @cfg {String} autoCreate @hide
1636 * @cfg {String} inputType @hide
1639 * @cfg {String} invalidClass @hide
1642 * @cfg {String} invalidText @hide
1645 * @cfg {String} msgFx @hide
1648 * @cfg {String} validateOnBlur @hide
1652 Roo.HtmlEditorCore.white = [
1653 'area', 'br', 'img', 'input', 'hr', 'wbr',
1655 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1656 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1657 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1658 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1659 'table', 'ul', 'xmp',
1661 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1664 'dir', 'menu', 'ol', 'ul', 'dl',
1670 Roo.HtmlEditorCore.black = [
1671 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1673 'base', 'basefont', 'bgsound', 'blink', 'body',
1674 'frame', 'frameset', 'head', 'html', 'ilayer',
1675 'iframe', 'layer', 'link', 'meta', 'object',
1676 'script', 'style' ,'title', 'xml' // clean later..
1678 Roo.HtmlEditorCore.clean = [
1679 'script', 'style', 'title', 'xml'
1681 Roo.HtmlEditorCore.remove = [
1686 Roo.HtmlEditorCore.ablack = [
1690 Roo.HtmlEditorCore.aclean = [
1691 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1695 Roo.HtmlEditorCore.pwhite= [
1696 'http', 'https', 'mailto'
1699 // white listed style attributes.
1700 Roo.HtmlEditorCore.cwhite= [
1701 // 'text-align', /// default is to allow most things..
1707 // black listed style attributes.
1708 Roo.HtmlEditorCore.cblack= [
1709 // 'font-size' -- this can be set by the project
1713 Roo.HtmlEditorCore.swapCodes =[