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(/([\x80-\uffff])/g, function (a, b) {
339 var cc = b.charCodeAt();
341 (cc >= 0x4E00 && cc < 0xA000 ) ||
342 (cc >= 0x3400 && cc < 0x4E00 ) ||
343 (cc >= 0xf900 && cc < 0xfb00 )
349 if(this.owner.fireEvent('beforesync', this, html) !== false){
350 this.el.dom.value = html;
351 this.owner.fireEvent('sync', this, html);
357 * Protected method that will not generally be called directly. Pushes the value of the textarea
358 * into the iframe editor.
360 pushValue : function(){
361 if(this.initialized){
362 var v = this.el.dom.value.trim();
368 if(this.owner.fireEvent('beforepush', this, v) !== false){
369 var d = (this.doc.body || this.doc.documentElement);
372 this.el.dom.value = d.innerHTML;
373 this.owner.fireEvent('push', this, v);
379 deferFocus : function(){
380 this.focus.defer(10, this);
385 if(this.win && !this.sourceEditMode){
392 assignDocWin: function()
394 var iframe = this.iframe;
397 this.doc = iframe.contentWindow.document;
398 this.win = iframe.contentWindow;
400 // if (!Roo.get(this.frameId)) {
403 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
404 // this.win = Roo.get(this.frameId).dom.contentWindow;
406 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
410 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
411 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
416 initEditor : function(){
417 //console.log("INIT EDITOR");
422 this.doc.designMode="on";
424 this.doc.write(this.getDocMarkup());
427 var dbody = (this.doc.body || this.doc.documentElement);
428 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
429 // this copies styles from the containing element into thsi one..
430 // not sure why we need all of this..
431 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
433 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
434 //ss['background-attachment'] = 'fixed'; // w3c
435 dbody.bgProperties = 'fixed'; // ie
436 //Roo.DomHelper.applyStyles(dbody, ss);
437 Roo.EventManager.on(this.doc, {
438 //'mousedown': this.onEditorEvent,
439 'mouseup': this.onEditorEvent,
440 'dblclick': this.onEditorEvent,
441 'click': this.onEditorEvent,
442 'keyup': this.onEditorEvent,
447 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
449 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
450 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
452 this.initialized = true;
454 this.owner.fireEvent('initialize', this);
459 onDestroy : function(){
465 //for (var i =0; i < this.toolbars.length;i++) {
466 // // fixme - ask toolbars for heights?
467 // this.toolbars[i].onDestroy();
470 //this.wrap.dom.innerHTML = '';
471 //this.wrap.remove();
476 onFirstFocus : function(){
481 this.activated = true;
484 if(Roo.isGecko){ // prevent silly gecko errors
486 var s = this.win.getSelection();
487 if(!s.focusNode || s.focusNode.nodeType != 3){
488 var r = s.getRangeAt(0);
489 r.selectNodeContents((this.doc.body || this.doc.documentElement));
494 this.execCmd('useCSS', true);
495 this.execCmd('styleWithCSS', false);
498 this.owner.fireEvent('activate', this);
502 adjustFont: function(btn){
503 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
504 //if(Roo.isSafari){ // safari
507 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
508 if(Roo.isSafari){ // safari
509 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
510 v = (v < 10) ? 10 : v;
511 v = (v > 48) ? 48 : v;
512 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
517 v = Math.max(1, v+adjust);
519 this.execCmd('FontSize', v );
522 onEditorEvent : function(e)
524 this.owner.fireEvent('editorevent', this, e);
525 // this.updateToolbar();
526 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
529 insertTag : function(tg)
531 // could be a bit smarter... -> wrap the current selected tRoo..
532 if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
534 range = this.createRange(this.getSelection());
535 var wrappingNode = this.doc.createElement(tg.toLowerCase());
536 wrappingNode.appendChild(range.extractContents());
537 range.insertNode(wrappingNode);
544 this.execCmd("formatblock", tg);
548 insertText : function(txt)
552 var range = this.createRange();
553 range.deleteContents();
554 //alert(Sender.getAttribute('label'));
556 range.insertNode(this.doc.createTextNode(txt));
562 * Executes a Midas editor command on the editor document and performs necessary focus and
563 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
564 * @param {String} cmd The Midas command
565 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
567 relayCmd : function(cmd, value){
569 this.execCmd(cmd, value);
570 this.owner.fireEvent('editorevent', this);
571 //this.updateToolbar();
572 this.owner.deferFocus();
576 * Executes a Midas editor command directly on the editor document.
577 * For visual commands, you should use {@link #relayCmd} instead.
578 * <b>This should only be called after the editor is initialized.</b>
579 * @param {String} cmd The Midas command
580 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
582 execCmd : function(cmd, value){
583 this.doc.execCommand(cmd, false, value === undefined ? null : value);
590 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
592 * @param {String} text | dom node..
594 insertAtCursor : function(text)
603 var r = this.doc.selection.createRange();
614 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
618 // from jquery ui (MIT licenced)
622 if (win.getSelection && win.getSelection().getRangeAt) {
623 range = win.getSelection().getRangeAt(0);
624 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
625 range.insertNode(node);
626 } else if (win.document.selection && win.document.selection.createRange) {
627 // no firefox support
628 var txt = typeof(text) == 'string' ? text : text.outerHTML;
629 win.document.selection.createRange().pasteHTML(txt);
631 // no firefox support
632 var txt = typeof(text) == 'string' ? text : text.outerHTML;
633 this.execCmd('InsertHTML', txt);
642 mozKeyPress : function(e){
644 var c = e.getCharCode(), cmd;
647 c = String.fromCharCode(c).toLowerCase();
661 this.cleanUpPaste.defer(100, this);
677 fixKeys : function(){ // load time branching for fastest keydown performance
680 var k = e.getKey(), r;
683 r = this.doc.selection.createRange();
686 r.pasteHTML('    ');
693 r = this.doc.selection.createRange();
695 var target = r.parentElement();
696 if(!target || target.tagName.toLowerCase() != 'li'){
698 r.pasteHTML('<br />');
704 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
705 this.cleanUpPaste.defer(100, this);
711 }else if(Roo.isOpera){
717 this.execCmd('InsertHTML','    ');
720 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
721 this.cleanUpPaste.defer(100, this);
726 }else if(Roo.isSafari){
732 this.execCmd('InsertText','\t');
736 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
737 this.cleanUpPaste.defer(100, this);
745 getAllAncestors: function()
747 var p = this.getSelectedNode();
750 a.push(p); // push blank onto stack..
751 p = this.getParentElement();
755 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
759 a.push(this.doc.body);
766 getSelection : function()
769 return Roo.isIE ? this.doc.selection : this.win.getSelection();
772 getSelectedNode: function()
774 // this may only work on Gecko!!!
776 // should we cache this!!!!
781 var range = this.createRange(this.getSelection()).cloneRange();
784 var parent = range.parentElement();
786 var testRange = range.duplicate();
787 testRange.moveToElementText(parent);
788 if (testRange.inRange(range)) {
791 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
794 parent = parent.parentElement;
799 // is ancestor a text element.
800 var ac = range.commonAncestorContainer;
801 if (ac.nodeType == 3) {
805 var ar = ac.childNodes;
808 var other_nodes = [];
809 var has_other_nodes = false;
810 for (var i=0;i<ar.length;i++) {
811 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
814 // fullly contained node.
816 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
821 // probably selected..
822 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
823 other_nodes.push(ar[i]);
827 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
832 has_other_nodes = true;
834 if (!nodes.length && other_nodes.length) {
837 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
843 createRange: function(sel)
845 // this has strange effects when using with
846 // top toolbar - not sure if it's a great idea.
847 //this.editor.contentWindow.focus();
848 if (typeof sel != "undefined") {
850 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
852 return this.doc.createRange();
855 return this.doc.createRange();
858 getParentElement: function()
862 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
864 var range = this.createRange(sel);
867 var p = range.commonAncestorContainer;
868 while (p.nodeType == 3) { // text node
879 * Range intersection.. the hard stuff...
883 * [ -- selected range --- ]
887 * if end is before start or hits it. fail.
888 * if start is after end or hits it fail.
890 * if either hits (but other is outside. - then it's not
896 // @see http://www.thismuchiknow.co.uk/?p=64.
897 rangeIntersectsNode : function(range, node)
899 var nodeRange = node.ownerDocument.createRange();
901 nodeRange.selectNode(node);
903 nodeRange.selectNodeContents(node);
906 var rangeStartRange = range.cloneRange();
907 rangeStartRange.collapse(true);
909 var rangeEndRange = range.cloneRange();
910 rangeEndRange.collapse(false);
912 var nodeStartRange = nodeRange.cloneRange();
913 nodeStartRange.collapse(true);
915 var nodeEndRange = nodeRange.cloneRange();
916 nodeEndRange.collapse(false);
918 return rangeStartRange.compareBoundaryPoints(
919 Range.START_TO_START, nodeEndRange) == -1 &&
920 rangeEndRange.compareBoundaryPoints(
921 Range.START_TO_START, nodeStartRange) == 1;
925 rangeCompareNode : function(range, node)
927 var nodeRange = node.ownerDocument.createRange();
929 nodeRange.selectNode(node);
931 nodeRange.selectNodeContents(node);
935 range.collapse(true);
937 nodeRange.collapse(true);
939 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
940 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
942 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
944 var nodeIsBefore = ss == 1;
945 var nodeIsAfter = ee == -1;
947 if (nodeIsBefore && nodeIsAfter) {
950 if (!nodeIsBefore && nodeIsAfter) {
951 return 1; //right trailed.
954 if (nodeIsBefore && !nodeIsAfter) {
955 return 2; // left trailed.
961 // private? - in a new class?
962 cleanUpPaste : function()
964 // cleans up the whole document..
965 Roo.log('cleanuppaste');
967 this.cleanUpChildren(this.doc.body);
968 var clean = this.cleanWordChars(this.doc.body.innerHTML);
969 if (clean != this.doc.body.innerHTML) {
970 this.doc.body.innerHTML = clean;
975 cleanWordChars : function(input) {// change the chars to hex code
976 var he = Roo.HtmlEditorCore;
979 Roo.each(he.swapCodes, function(sw) {
980 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
982 output = output.replace(swapper, sw[1]);
989 cleanUpChildren : function (n)
991 if (!n.childNodes.length) {
994 for (var i = n.childNodes.length-1; i > -1 ; i--) {
995 this.cleanUpChild(n.childNodes[i]);
1002 cleanUpChild : function (node)
1005 //console.log(node);
1006 if (node.nodeName == "#text") {
1007 // clean up silly Windows -- stuff?
1010 if (node.nodeName == "#comment") {
1011 node.parentNode.removeChild(node);
1012 // clean up silly Windows -- stuff?
1015 var lcname = node.tagName.toLowerCase();
1016 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1017 // whitelist of tags..
1019 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1021 node.parentNode.removeChild(node);
1026 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1028 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1029 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1031 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1032 // remove_keep_children = true;
1035 if (remove_keep_children) {
1036 this.cleanUpChildren(node);
1037 // inserts everything just before this node...
1038 while (node.childNodes.length) {
1039 var cn = node.childNodes[0];
1040 node.removeChild(cn);
1041 node.parentNode.insertBefore(cn, node);
1043 node.parentNode.removeChild(node);
1047 if (!node.attributes || !node.attributes.length) {
1048 this.cleanUpChildren(node);
1052 function cleanAttr(n,v)
1055 if (v.match(/^\./) || v.match(/^\//)) {
1058 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1061 if (v.match(/^#/)) {
1064 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1065 node.removeAttribute(n);
1069 var cwhite = this.cwhite;
1070 var cblack = this.cblack;
1072 function cleanStyle(n,v)
1074 if (v.match(/expression/)) { //XSS?? should we even bother..
1075 node.removeAttribute(n);
1079 var parts = v.split(/;/);
1082 Roo.each(parts, function(p) {
1083 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1087 var l = p.split(':').shift().replace(/\s+/g,'');
1088 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1090 if ( cwhite.length && cblack.indexOf(l) > -1) {
1091 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1092 //node.removeAttribute(n);
1096 // only allow 'c whitelisted system attributes'
1097 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1098 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1099 //node.removeAttribute(n);
1110 node.setAttribute(n, clean.join(';'));
1112 node.removeAttribute(n);
1118 for (var i = node.attributes.length-1; i > -1 ; i--) {
1119 var a = node.attributes[i];
1122 if (a.name.toLowerCase().substr(0,2)=='on') {
1123 node.removeAttribute(a.name);
1126 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1127 node.removeAttribute(a.name);
1130 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1131 cleanAttr(a.name,a.value); // fixme..
1134 if (a.name == 'style') {
1135 cleanStyle(a.name,a.value);
1138 /// clean up MS crap..
1139 // tecnically this should be a list of valid class'es..
1142 if (a.name == 'class') {
1143 if (a.value.match(/^Mso/)) {
1144 node.className = '';
1147 if (a.value.match(/^body$/)) {
1148 node.className = '';
1159 this.cleanUpChildren(node);
1165 * Clean up MS wordisms...
1167 cleanWord : function(node)
1172 this.cleanWord(this.doc.body);
1175 if (node.nodeName == "#text") {
1176 // clean up silly Windows -- stuff?
1179 if (node.nodeName == "#comment") {
1180 node.parentNode.removeChild(node);
1181 // clean up silly Windows -- stuff?
1185 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1186 node.parentNode.removeChild(node);
1190 // remove - but keep children..
1191 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1192 while (node.childNodes.length) {
1193 var cn = node.childNodes[0];
1194 node.removeChild(cn);
1195 node.parentNode.insertBefore(cn, node);
1197 node.parentNode.removeChild(node);
1198 this.iterateChildren(node, this.cleanWord);
1202 if (node.className.length) {
1204 var cn = node.className.split(/\W+/);
1206 Roo.each(cn, function(cls) {
1207 if (cls.match(/Mso[a-zA-Z]+/)) {
1212 node.className = cna.length ? cna.join(' ') : '';
1214 node.removeAttribute("class");
1218 if (node.hasAttribute("lang")) {
1219 node.removeAttribute("lang");
1222 if (node.hasAttribute("style")) {
1224 var styles = node.getAttribute("style").split(";");
1226 Roo.each(styles, function(s) {
1227 if (!s.match(/:/)) {
1230 var kv = s.split(":");
1231 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1234 // what ever is left... we allow.
1237 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1238 if (!nstyle.length) {
1239 node.removeAttribute('style');
1242 this.iterateChildren(node, this.cleanWord);
1248 * iterateChildren of a Node, calling fn each time, using this as the scole..
1249 * @param {DomNode} node node to iterate children of.
1250 * @param {Function} fn method of this class to call on each item.
1252 iterateChildren : function(node, fn)
1254 if (!node.childNodes.length) {
1257 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1258 fn.call(this, node.childNodes[i])
1266 * Quite often pasting from word etc.. results in tables with column and widths.
1267 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1270 cleanTableWidths : function(node)
1275 this.cleanTableWidths(this.doc.body);
1280 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1283 Roo.log(node.tagName);
1284 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1285 this.iterateChildren(node, this.cleanTableWidths);
1288 if (node.hasAttribute('width')) {
1289 node.removeAttribute('width');
1293 if (node.hasAttribute("style")) {
1296 var styles = node.getAttribute("style").split(";");
1298 Roo.each(styles, function(s) {
1299 if (!s.match(/:/)) {
1302 var kv = s.split(":");
1303 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1306 // what ever is left... we allow.
1309 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1310 if (!nstyle.length) {
1311 node.removeAttribute('style');
1315 this.iterateChildren(node, this.cleanTableWidths);
1323 domToHTML : function(currentElement, depth, nopadtext) {
1326 nopadtext = nopadtext || false;
1328 if (!currentElement) {
1329 return this.domToHTML(this.doc.body);
1332 //Roo.log(currentElement);
1334 var allText = false;
1335 var nodeName = currentElement.nodeName;
1336 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1338 if (nodeName == '#text') {
1340 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1345 if (nodeName != 'BODY') {
1348 // Prints the node tagName, such as <A>, <IMG>, etc
1351 for(i = 0; i < currentElement.attributes.length;i++) {
1353 var aname = currentElement.attributes.item(i).name;
1354 if (!currentElement.attributes.item(i).value.length) {
1357 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1360 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1369 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1372 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1377 // Traverse the tree
1379 var currentElementChild = currentElement.childNodes.item(i);
1383 while (currentElementChild) {
1384 // Formatting code (indent the tree so it looks nice on the screen)
1385 var nopad = nopadtext;
1386 if (lastnode == 'SPAN') {
1390 if (currentElementChild.nodeName == '#text') {
1391 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1392 toadd = nopadtext ? toadd : toadd.trim();
1393 if (!nopad && toadd.length > 80) {
1394 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1399 currentElementChild = currentElement.childNodes.item(i);
1405 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1407 // Recursively traverse the tree structure of the child node
1408 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1409 lastnode = currentElementChild.nodeName;
1411 currentElementChild=currentElement.childNodes.item(i);
1417 // The remaining code is mostly for formatting the tree
1418 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1423 ret+= "</"+tagName+">";
1429 applyBlacklists : function()
1431 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1432 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1436 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1437 if (b.indexOf(tag) > -1) {
1440 this.white.push(tag);
1444 Roo.each(w, function(tag) {
1445 if (b.indexOf(tag) > -1) {
1448 if (this.white.indexOf(tag) > -1) {
1451 this.white.push(tag);
1456 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1457 if (w.indexOf(tag) > -1) {
1460 this.black.push(tag);
1464 Roo.each(b, function(tag) {
1465 if (w.indexOf(tag) > -1) {
1468 if (this.black.indexOf(tag) > -1) {
1471 this.black.push(tag);
1476 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1477 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1481 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1482 if (b.indexOf(tag) > -1) {
1485 this.cwhite.push(tag);
1489 Roo.each(w, function(tag) {
1490 if (b.indexOf(tag) > -1) {
1493 if (this.cwhite.indexOf(tag) > -1) {
1496 this.cwhite.push(tag);
1501 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1502 if (w.indexOf(tag) > -1) {
1505 this.cblack.push(tag);
1509 Roo.each(b, function(tag) {
1510 if (w.indexOf(tag) > -1) {
1513 if (this.cblack.indexOf(tag) > -1) {
1516 this.cblack.push(tag);
1521 setStylesheets : function(stylesheets)
1523 if(typeof(stylesheets) == 'string'){
1524 Roo.get(this.iframe.contentDocument.head).createChild({
1535 Roo.each(stylesheets, function(s) {
1540 Roo.get(_this.iframe.contentDocument.head).createChild({
1551 removeStylesheets : function()
1555 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1560 setStyle : function(style)
1562 Roo.get(this.iframe.contentDocument.head).createChild({
1571 // hide stuff that is not compatible
1589 * @cfg {String} fieldClass @hide
1592 * @cfg {String} focusClass @hide
1595 * @cfg {String} autoCreate @hide
1598 * @cfg {String} inputType @hide
1601 * @cfg {String} invalidClass @hide
1604 * @cfg {String} invalidText @hide
1607 * @cfg {String} msgFx @hide
1610 * @cfg {String} validateOnBlur @hide
1614 Roo.HtmlEditorCore.white = [
1615 'area', 'br', 'img', 'input', 'hr', 'wbr',
1617 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1618 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1619 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1620 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1621 'table', 'ul', 'xmp',
1623 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1626 'dir', 'menu', 'ol', 'ul', 'dl',
1632 Roo.HtmlEditorCore.black = [
1633 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1635 'base', 'basefont', 'bgsound', 'blink', 'body',
1636 'frame', 'frameset', 'head', 'html', 'ilayer',
1637 'iframe', 'layer', 'link', 'meta', 'object',
1638 'script', 'style' ,'title', 'xml' // clean later..
1640 Roo.HtmlEditorCore.clean = [
1641 'script', 'style', 'title', 'xml'
1643 Roo.HtmlEditorCore.remove = [
1648 Roo.HtmlEditorCore.ablack = [
1652 Roo.HtmlEditorCore.aclean = [
1653 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1657 Roo.HtmlEditorCore.pwhite= [
1658 'http', 'https', 'mailto'
1661 // white listed style attributes.
1662 Roo.HtmlEditorCore.cwhite= [
1663 // 'text-align', /// default is to allow most things..
1669 // black listed style attributes.
1670 Roo.HtmlEditorCore.cblack= [
1671 // 'font-size' -- this can be set by the project
1675 Roo.HtmlEditorCore.swapCodes =[