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)
1170 this.cleanWord(this.doc.body);
1174 if(node.nodeName == 'SPAN') {
1176 console.log(node.attributes);
1177 console.log(node.classList);
1178 console.log(node.childNodes);
1179 console.log(node.childNodes[0].nodeName);
1182 if (node.nodeName == "#text") {
1183 // clean up silly Windows -- stuff?
1186 if (node.nodeName == "#comment") {
1187 node.parentNode.removeChild(node);
1188 // clean up silly Windows -- stuff?
1192 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1193 node.parentNode.removeChild(node);
1197 // remove - but keep children..
1198 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1199 while (node.childNodes.length) {
1200 var cn = node.childNodes[0];
1201 node.removeChild(cn);
1202 node.parentNode.insertBefore(cn, node);
1204 node.parentNode.removeChild(node);
1205 this.iterateChildren(node, this.cleanWord);
1209 if (node.className.length) {
1211 var cn = node.className.split(/\W+/);
1213 Roo.each(cn, function(cls) {
1214 if (cls.match(/Mso[a-zA-Z]+/)) {
1219 node.className = cna.length ? cna.join(' ') : '';
1221 node.removeAttribute("class");
1225 if (node.hasAttribute("lang")) {
1226 node.removeAttribute("lang");
1229 if (node.hasAttribute("style")) {
1231 var styles = node.getAttribute("style").split(";");
1233 Roo.each(styles, function(s) {
1234 if (!s.match(/:/)) {
1237 var kv = s.split(":");
1238 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1241 // what ever is left... we allow.
1244 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1245 if (!nstyle.length) {
1246 node.removeAttribute('style');
1249 this.iterateChildren(node, this.cleanWord);
1255 * iterateChildren of a Node, calling fn each time, using this as the scole..
1256 * @param {DomNode} node node to iterate children of.
1257 * @param {Function} fn method of this class to call on each item.
1259 iterateChildren : function(node, fn)
1261 if (!node.childNodes.length) {
1264 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1265 fn.call(this, node.childNodes[i])
1273 * Quite often pasting from word etc.. results in tables with column and widths.
1274 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1277 cleanTableWidths : function(node)
1282 this.cleanTableWidths(this.doc.body);
1287 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1290 Roo.log(node.tagName);
1291 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1292 this.iterateChildren(node, this.cleanTableWidths);
1295 if (node.hasAttribute('width')) {
1296 node.removeAttribute('width');
1300 if (node.hasAttribute("style")) {
1303 var styles = node.getAttribute("style").split(";");
1305 Roo.each(styles, function(s) {
1306 if (!s.match(/:/)) {
1309 var kv = s.split(":");
1310 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1313 // what ever is left... we allow.
1316 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1317 if (!nstyle.length) {
1318 node.removeAttribute('style');
1322 this.iterateChildren(node, this.cleanTableWidths);
1330 domToHTML : function(currentElement, depth, nopadtext) {
1333 nopadtext = nopadtext || false;
1335 if (!currentElement) {
1336 return this.domToHTML(this.doc.body);
1339 //Roo.log(currentElement);
1341 var allText = false;
1342 var nodeName = currentElement.nodeName;
1343 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1345 if (nodeName == '#text') {
1347 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1352 if (nodeName != 'BODY') {
1355 // Prints the node tagName, such as <A>, <IMG>, etc
1358 for(i = 0; i < currentElement.attributes.length;i++) {
1360 var aname = currentElement.attributes.item(i).name;
1361 if (!currentElement.attributes.item(i).value.length) {
1364 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1367 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1376 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1379 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1384 // Traverse the tree
1386 var currentElementChild = currentElement.childNodes.item(i);
1390 while (currentElementChild) {
1391 // Formatting code (indent the tree so it looks nice on the screen)
1392 var nopad = nopadtext;
1393 if (lastnode == 'SPAN') {
1397 if (currentElementChild.nodeName == '#text') {
1398 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1399 toadd = nopadtext ? toadd : toadd.trim();
1400 if (!nopad && toadd.length > 80) {
1401 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1406 currentElementChild = currentElement.childNodes.item(i);
1412 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1414 // Recursively traverse the tree structure of the child node
1415 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1416 lastnode = currentElementChild.nodeName;
1418 currentElementChild=currentElement.childNodes.item(i);
1424 // The remaining code is mostly for formatting the tree
1425 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1430 ret+= "</"+tagName+">";
1436 applyBlacklists : function()
1438 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1439 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1443 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1444 if (b.indexOf(tag) > -1) {
1447 this.white.push(tag);
1451 Roo.each(w, function(tag) {
1452 if (b.indexOf(tag) > -1) {
1455 if (this.white.indexOf(tag) > -1) {
1458 this.white.push(tag);
1463 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1464 if (w.indexOf(tag) > -1) {
1467 this.black.push(tag);
1471 Roo.each(b, function(tag) {
1472 if (w.indexOf(tag) > -1) {
1475 if (this.black.indexOf(tag) > -1) {
1478 this.black.push(tag);
1483 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1484 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1488 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1489 if (b.indexOf(tag) > -1) {
1492 this.cwhite.push(tag);
1496 Roo.each(w, function(tag) {
1497 if (b.indexOf(tag) > -1) {
1500 if (this.cwhite.indexOf(tag) > -1) {
1503 this.cwhite.push(tag);
1508 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1509 if (w.indexOf(tag) > -1) {
1512 this.cblack.push(tag);
1516 Roo.each(b, function(tag) {
1517 if (w.indexOf(tag) > -1) {
1520 if (this.cblack.indexOf(tag) > -1) {
1523 this.cblack.push(tag);
1528 setStylesheets : function(stylesheets)
1530 if(typeof(stylesheets) == 'string'){
1531 Roo.get(this.iframe.contentDocument.head).createChild({
1542 Roo.each(stylesheets, function(s) {
1547 Roo.get(_this.iframe.contentDocument.head).createChild({
1558 removeStylesheets : function()
1562 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1567 setStyle : function(style)
1569 Roo.get(this.iframe.contentDocument.head).createChild({
1578 // hide stuff that is not compatible
1596 * @cfg {String} fieldClass @hide
1599 * @cfg {String} focusClass @hide
1602 * @cfg {String} autoCreate @hide
1605 * @cfg {String} inputType @hide
1608 * @cfg {String} invalidClass @hide
1611 * @cfg {String} invalidText @hide
1614 * @cfg {String} msgFx @hide
1617 * @cfg {String} validateOnBlur @hide
1621 Roo.HtmlEditorCore.white = [
1622 'area', 'br', 'img', 'input', 'hr', 'wbr',
1624 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1625 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1626 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1627 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1628 'table', 'ul', 'xmp',
1630 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1633 'dir', 'menu', 'ol', 'ul', 'dl',
1639 Roo.HtmlEditorCore.black = [
1640 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1642 'base', 'basefont', 'bgsound', 'blink', 'body',
1643 'frame', 'frameset', 'head', 'html', 'ilayer',
1644 'iframe', 'layer', 'link', 'meta', 'object',
1645 'script', 'style' ,'title', 'xml' // clean later..
1647 Roo.HtmlEditorCore.clean = [
1648 'script', 'style', 'title', 'xml'
1650 Roo.HtmlEditorCore.remove = [
1655 Roo.HtmlEditorCore.ablack = [
1659 Roo.HtmlEditorCore.aclean = [
1660 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1664 Roo.HtmlEditorCore.pwhite= [
1665 'http', 'https', 'mailto'
1668 // white listed style attributes.
1669 Roo.HtmlEditorCore.cwhite= [
1670 // 'text-align', /// default is to allow most things..
1676 // black listed style attributes.
1677 Roo.HtmlEditorCore.cblack= [
1678 // 'font-size' -- this can be set by the project
1682 Roo.HtmlEditorCore.swapCodes =[