1 //<script type="text/javascript">
4 * Based Ext JS Library 1.1.1
5 * Copyright(c) 2006-2007, Ext JS, LLC.
11 * @class Roo.HtmlEditorCore
12 * @extends Roo.Component
13 * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
15 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
18 Roo.HtmlEditorCore = function(config){
21 Roo.HtmlEditorCore.superclass.constructor.call(this, config);
27 * Fires when the editor is fully initialized (including the iframe)
28 * @param {Roo.HtmlEditorCore} this
33 * Fires when the editor is first receives the focus. Any insertion must wait
34 * until after this event.
35 * @param {Roo.HtmlEditorCore} this
40 * Fires before the textarea is updated with content from the editor iframe. Return false
42 * @param {Roo.HtmlEditorCore} this
43 * @param {String} html
48 * Fires before the iframe editor is updated with content from the textarea. Return false
50 * @param {Roo.HtmlEditorCore} this
51 * @param {String} html
56 * Fires when the textarea is updated with content from the editor iframe.
57 * @param {Roo.HtmlEditorCore} this
58 * @param {String} html
63 * Fires when the iframe editor is updated with content from the textarea.
64 * @param {Roo.HtmlEditorCore} this
65 * @param {String} html
71 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72 * @param {Roo.HtmlEditorCore} this
78 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80 // defaults : white / black...
81 this.applyBlacklists();
88 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
92 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
98 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
103 * @cfg {Number} height (in pixels)
107 * @cfg {Number} width (in pixels)
112 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
120 // private properties
121 validationEvent : false,
125 sourceEditMode : false,
126 onFocus : Roo.emptyFn,
132 // blacklist + whitelisted elements..
139 * Protected method that will not generally be called directly. It
140 * is called when the editor initializes the iframe with HTML contents. Override this method if you
141 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
143 getDocMarkup : function(){
147 // inherit styels from page...??
148 if (this.stylesheets === false) {
150 Roo.get(document.head).select('style').each(function(node) {
151 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
154 Roo.get(document.head).select('link').each(function(node) {
155 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
158 } else if (!this.stylesheets.length) {
160 st = '<style type="text/css">' +
161 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
167 st += '<style type="text/css">' +
168 'IMG { cursor: pointer } ' +
172 return '<html><head>' + st +
173 //<style type="text/css">' +
174 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
176 ' </head><body class="roo-htmleditor-body"></body></html>';
180 onRender : function(ct, position)
183 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
184 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
187 this.el.dom.style.border = '0 none';
188 this.el.dom.setAttribute('tabIndex', -1);
189 this.el.addClass('x-hidden hide');
193 if(Roo.isIE){ // fix IE 1px bogus margin
194 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
198 this.frameId = Roo.id();
202 var iframe = this.owner.wrap.createChild({
204 cls: 'form-control', // bootstrap..
208 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
213 this.iframe = iframe.dom;
217 this.doc.designMode = 'on';
220 this.doc.write(this.getDocMarkup());
224 var task = { // must defer to wait for browser to be ready
226 //console.log("run task?" + this.doc.readyState);
228 if(this.doc.body || this.doc.readyState == 'complete'){
230 this.doc.designMode="on";
234 Roo.TaskMgr.stop(task);
235 this.initEditor.defer(10, this);
242 Roo.TaskMgr.start(task);
247 onResize : function(w, h)
249 Roo.log('resize: ' +w + ',' + h );
250 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
254 if(typeof w == 'number'){
256 this.iframe.style.width = w + 'px';
258 if(typeof h == 'number'){
260 this.iframe.style.height = h + 'px';
262 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
269 * Toggles the editor between standard and source edit mode.
270 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
272 toggleSourceEdit : function(sourceEditMode){
274 this.sourceEditMode = sourceEditMode === true;
276 if(this.sourceEditMode){
278 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
281 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
282 //this.iframe.className = '';
285 //this.setSize(this.owner.wrap.getSize());
286 //this.fireEvent('editmodechange', this, this.sourceEditMode);
293 * Protected method that will not generally be called directly. If you need/want
294 * custom HTML cleanup, this is the method you should override.
295 * @param {String} html The HTML to be cleaned
296 * return {String} The cleaned HTML
298 cleanHtml : function(html){
301 if(Roo.isSafari){ // strip safari nonsense
302 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
305 if(html == ' '){
312 * HTML Editor -> Textarea
313 * Protected method that will not generally be called directly. Syncs the contents
314 * of the editor iframe with the textarea.
316 syncValue : function(){
317 if(this.initialized){
318 var bd = (this.doc.body || this.doc.documentElement);
319 //this.cleanUpPaste(); -- this is done else where and causes havoc..
320 var html = bd.innerHTML;
322 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
323 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
325 html = '<div style="'+m[0]+'">' + html + '</div>';
328 html = this.cleanHtml(html);
329 // fix up the special chars.. normaly like back quotes in word...
330 // however we do not want to do this with chinese..
331 html = html.replace(/([\x80-\uffff])/g, function (a, b) {
332 var cc = b.charCodeAt();
334 (cc >= 0x4E00 && cc < 0xA000 ) ||
335 (cc >= 0x3400 && cc < 0x4E00 ) ||
336 (cc >= 0xf900 && cc < 0xfb00 )
342 if(this.owner.fireEvent('beforesync', this, html) !== false){
343 this.el.dom.value = html;
344 this.owner.fireEvent('sync', this, html);
350 * Protected method that will not generally be called directly. Pushes the value of the textarea
351 * into the iframe editor.
353 pushValue : function(){
354 if(this.initialized){
355 var v = this.el.dom.value.trim();
361 if(this.owner.fireEvent('beforepush', this, v) !== false){
362 var d = (this.doc.body || this.doc.documentElement);
366 this.el.dom.value = d.innerHTML;
367 this.owner.fireEvent('push', this, v);
373 deferFocus : function(){
374 this.focus.defer(10, this);
379 if(this.win && !this.sourceEditMode){
386 assignDocWin: function()
388 var iframe = this.iframe;
391 this.doc = iframe.contentWindow.document;
392 this.win = iframe.contentWindow;
394 // if (!Roo.get(this.frameId)) {
397 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
398 // this.win = Roo.get(this.frameId).dom.contentWindow;
400 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
404 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
405 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
410 initEditor : function(){
411 //console.log("INIT EDITOR");
416 this.doc.designMode="on";
418 this.doc.write(this.getDocMarkup());
421 var dbody = (this.doc.body || this.doc.documentElement);
422 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
423 // this copies styles from the containing element into thsi one..
424 // not sure why we need all of this..
425 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
427 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
428 //ss['background-attachment'] = 'fixed'; // w3c
429 dbody.bgProperties = 'fixed'; // ie
430 //Roo.DomHelper.applyStyles(dbody, ss);
431 Roo.EventManager.on(this.doc, {
432 //'mousedown': this.onEditorEvent,
433 'mouseup': this.onEditorEvent,
434 'dblclick': this.onEditorEvent,
435 'click': this.onEditorEvent,
436 'keyup': this.onEditorEvent,
441 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
443 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
444 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
446 this.initialized = true;
448 this.owner.fireEvent('initialize', this);
453 onDestroy : function(){
459 //for (var i =0; i < this.toolbars.length;i++) {
460 // // fixme - ask toolbars for heights?
461 // this.toolbars[i].onDestroy();
464 //this.wrap.dom.innerHTML = '';
465 //this.wrap.remove();
470 onFirstFocus : function(){
475 this.activated = true;
478 if(Roo.isGecko){ // prevent silly gecko errors
480 var s = this.win.getSelection();
481 if(!s.focusNode || s.focusNode.nodeType != 3){
482 var r = s.getRangeAt(0);
483 r.selectNodeContents((this.doc.body || this.doc.documentElement));
488 this.execCmd('useCSS', true);
489 this.execCmd('styleWithCSS', false);
492 this.owner.fireEvent('activate', this);
496 adjustFont: function(btn){
497 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
498 //if(Roo.isSafari){ // safari
501 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
502 if(Roo.isSafari){ // safari
503 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
504 v = (v < 10) ? 10 : v;
505 v = (v > 48) ? 48 : v;
506 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
511 v = Math.max(1, v+adjust);
513 this.execCmd('FontSize', v );
516 onEditorEvent : function(e)
518 this.owner.fireEvent('editorevent', this, e);
519 // this.updateToolbar();
520 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
523 insertTag : function(tg)
525 // could be a bit smarter... -> wrap the current selected tRoo..
526 if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
528 range = this.createRange(this.getSelection());
529 var wrappingNode = this.doc.createElement(tg.toLowerCase());
530 wrappingNode.appendChild(range.extractContents());
531 range.insertNode(wrappingNode);
538 this.execCmd("formatblock", tg);
542 insertText : function(txt)
546 var range = this.createRange();
547 range.deleteContents();
548 //alert(Sender.getAttribute('label'));
550 range.insertNode(this.doc.createTextNode(txt));
556 * Executes a Midas editor command on the editor document and performs necessary focus and
557 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
558 * @param {String} cmd The Midas command
559 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
561 relayCmd : function(cmd, value){
563 this.execCmd(cmd, value);
564 this.owner.fireEvent('editorevent', this);
565 //this.updateToolbar();
566 this.owner.deferFocus();
570 * Executes a Midas editor command directly on the editor document.
571 * For visual commands, you should use {@link #relayCmd} instead.
572 * <b>This should only be called after the editor is initialized.</b>
573 * @param {String} cmd The Midas command
574 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
576 execCmd : function(cmd, value){
577 this.doc.execCommand(cmd, false, value === undefined ? null : value);
584 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
586 * @param {String} text | dom node..
588 insertAtCursor : function(text)
597 var r = this.doc.selection.createRange();
608 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
612 // from jquery ui (MIT licenced)
616 if (win.getSelection && win.getSelection().getRangeAt) {
617 range = win.getSelection().getRangeAt(0);
618 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
619 range.insertNode(node);
620 } else if (win.document.selection && win.document.selection.createRange) {
621 // no firefox support
622 var txt = typeof(text) == 'string' ? text : text.outerHTML;
623 win.document.selection.createRange().pasteHTML(txt);
625 // no firefox support
626 var txt = typeof(text) == 'string' ? text : text.outerHTML;
627 this.execCmd('InsertHTML', txt);
636 mozKeyPress : function(e){
638 var c = e.getCharCode(), cmd;
641 c = String.fromCharCode(c).toLowerCase();
655 this.cleanUpPaste.defer(100, this);
671 fixKeys : function(){ // load time branching for fastest keydown performance
674 var k = e.getKey(), r;
677 r = this.doc.selection.createRange();
680 r.pasteHTML('    ');
687 r = this.doc.selection.createRange();
689 var target = r.parentElement();
690 if(!target || target.tagName.toLowerCase() != 'li'){
692 r.pasteHTML('<br />');
698 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
699 this.cleanUpPaste.defer(100, this);
705 }else if(Roo.isOpera){
711 this.execCmd('InsertHTML','    ');
714 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
715 this.cleanUpPaste.defer(100, this);
720 }else if(Roo.isSafari){
726 this.execCmd('InsertText','\t');
730 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
731 this.cleanUpPaste.defer(100, this);
739 getAllAncestors: function()
741 var p = this.getSelectedNode();
744 a.push(p); // push blank onto stack..
745 p = this.getParentElement();
749 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
753 a.push(this.doc.body);
760 getSelection : function()
763 return Roo.isIE ? this.doc.selection : this.win.getSelection();
766 getSelectedNode: function()
768 // this may only work on Gecko!!!
770 // should we cache this!!!!
775 var range = this.createRange(this.getSelection()).cloneRange();
778 var parent = range.parentElement();
780 var testRange = range.duplicate();
781 testRange.moveToElementText(parent);
782 if (testRange.inRange(range)) {
785 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
788 parent = parent.parentElement;
793 // is ancestor a text element.
794 var ac = range.commonAncestorContainer;
795 if (ac.nodeType == 3) {
799 var ar = ac.childNodes;
802 var other_nodes = [];
803 var has_other_nodes = false;
804 for (var i=0;i<ar.length;i++) {
805 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
808 // fullly contained node.
810 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
815 // probably selected..
816 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
817 other_nodes.push(ar[i]);
821 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
826 has_other_nodes = true;
828 if (!nodes.length && other_nodes.length) {
831 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
837 createRange: function(sel)
839 // this has strange effects when using with
840 // top toolbar - not sure if it's a great idea.
841 //this.editor.contentWindow.focus();
842 if (typeof sel != "undefined") {
844 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
846 return this.doc.createRange();
849 return this.doc.createRange();
852 getParentElement: function()
856 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
858 var range = this.createRange(sel);
861 var p = range.commonAncestorContainer;
862 while (p.nodeType == 3) { // text node
873 * Range intersection.. the hard stuff...
877 * [ -- selected range --- ]
881 * if end is before start or hits it. fail.
882 * if start is after end or hits it fail.
884 * if either hits (but other is outside. - then it's not
890 // @see http://www.thismuchiknow.co.uk/?p=64.
891 rangeIntersectsNode : function(range, node)
893 var nodeRange = node.ownerDocument.createRange();
895 nodeRange.selectNode(node);
897 nodeRange.selectNodeContents(node);
900 var rangeStartRange = range.cloneRange();
901 rangeStartRange.collapse(true);
903 var rangeEndRange = range.cloneRange();
904 rangeEndRange.collapse(false);
906 var nodeStartRange = nodeRange.cloneRange();
907 nodeStartRange.collapse(true);
909 var nodeEndRange = nodeRange.cloneRange();
910 nodeEndRange.collapse(false);
912 return rangeStartRange.compareBoundaryPoints(
913 Range.START_TO_START, nodeEndRange) == -1 &&
914 rangeEndRange.compareBoundaryPoints(
915 Range.START_TO_START, nodeStartRange) == 1;
919 rangeCompareNode : function(range, node)
921 var nodeRange = node.ownerDocument.createRange();
923 nodeRange.selectNode(node);
925 nodeRange.selectNodeContents(node);
929 range.collapse(true);
931 nodeRange.collapse(true);
933 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
934 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
936 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
938 var nodeIsBefore = ss == 1;
939 var nodeIsAfter = ee == -1;
941 if (nodeIsBefore && nodeIsAfter) {
944 if (!nodeIsBefore && nodeIsAfter) {
945 return 1; //right trailed.
948 if (nodeIsBefore && !nodeIsAfter) {
949 return 2; // left trailed.
955 // private? - in a new class?
956 cleanUpPaste : function()
958 // cleans up the whole document..
959 Roo.log('cleanuppaste');
961 this.cleanUpChildren(this.doc.body);
962 var clean = this.cleanWordChars(this.doc.body.innerHTML);
963 if (clean != this.doc.body.innerHTML) {
964 this.doc.body.innerHTML = clean;
969 cleanWordChars : function(input) {// change the chars to hex code
970 var he = Roo.HtmlEditorCore;
973 Roo.each(he.swapCodes, function(sw) {
974 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
976 output = output.replace(swapper, sw[1]);
983 cleanUpChildren : function (n)
985 if (!n.childNodes.length) {
988 for (var i = n.childNodes.length-1; i > -1 ; i--) {
989 this.cleanUpChild(n.childNodes[i]);
996 cleanUpChild : function (node)
1000 if (node.nodeName == "#text") {
1001 // clean up silly Windows -- stuff?
1004 if (node.nodeName == "#comment") {
1005 node.parentNode.removeChild(node);
1006 // clean up silly Windows -- stuff?
1009 var lcname = node.tagName.toLowerCase();
1010 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1011 // whitelist of tags..
1013 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1015 node.parentNode.removeChild(node);
1020 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1022 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1023 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1025 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1026 // remove_keep_children = true;
1029 if (remove_keep_children) {
1030 this.cleanUpChildren(node);
1031 // inserts everything just before this node...
1032 while (node.childNodes.length) {
1033 var cn = node.childNodes[0];
1034 node.removeChild(cn);
1035 node.parentNode.insertBefore(cn, node);
1037 node.parentNode.removeChild(node);
1041 if (!node.attributes || !node.attributes.length) {
1042 this.cleanUpChildren(node);
1046 function cleanAttr(n,v)
1049 if (v.match(/^\./) || v.match(/^\//)) {
1052 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/)) {
1055 if (v.match(/^#/)) {
1058 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1059 node.removeAttribute(n);
1063 var cwhite = this.cwhite;
1064 var cblack = this.cblack;
1066 function cleanStyle(n,v)
1068 if (v.match(/expression/)) { //XSS?? should we even bother..
1069 node.removeAttribute(n);
1073 var parts = v.split(/;/);
1076 Roo.each(parts, function(p) {
1077 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1081 var l = p.split(':').shift().replace(/\s+/g,'');
1082 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1084 if ( cwhite.length && cblack.indexOf(l) > -1) {
1085 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1086 //node.removeAttribute(n);
1090 // only allow 'c whitelisted system attributes'
1091 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1092 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1093 //node.removeAttribute(n);
1104 node.setAttribute(n, clean.join(';'));
1106 node.removeAttribute(n);
1112 for (var i = node.attributes.length-1; i > -1 ; i--) {
1113 var a = node.attributes[i];
1116 if (a.name.toLowerCase().substr(0,2)=='on') {
1117 node.removeAttribute(a.name);
1120 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1121 node.removeAttribute(a.name);
1124 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1125 cleanAttr(a.name,a.value); // fixme..
1128 if (a.name == 'style') {
1129 cleanStyle(a.name,a.value);
1132 /// clean up MS crap..
1133 // tecnically this should be a list of valid class'es..
1136 if (a.name == 'class') {
1137 if (a.value.match(/^Mso/)) {
1138 node.className = '';
1141 if (a.value.match(/body/)) {
1142 node.className = '';
1153 this.cleanUpChildren(node);
1159 * Clean up MS wordisms...
1161 cleanWord : function(node)
1166 this.cleanWord(this.doc.body);
1169 if (node.nodeName == "#text") {
1170 // clean up silly Windows -- stuff?
1173 if (node.nodeName == "#comment") {
1174 node.parentNode.removeChild(node);
1175 // clean up silly Windows -- stuff?
1179 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1180 node.parentNode.removeChild(node);
1184 // remove - but keep children..
1185 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1186 while (node.childNodes.length) {
1187 var cn = node.childNodes[0];
1188 node.removeChild(cn);
1189 node.parentNode.insertBefore(cn, node);
1191 node.parentNode.removeChild(node);
1192 this.iterateChildren(node, this.cleanWord);
1196 if (node.className.length) {
1198 var cn = node.className.split(/\W+/);
1200 Roo.each(cn, function(cls) {
1201 if (cls.match(/Mso[a-zA-Z]+/)) {
1206 node.className = cna.length ? cna.join(' ') : '';
1208 node.removeAttribute("class");
1212 if (node.hasAttribute("lang")) {
1213 node.removeAttribute("lang");
1216 if (node.hasAttribute("style")) {
1218 var styles = node.getAttribute("style").split(";");
1220 Roo.each(styles, function(s) {
1221 if (!s.match(/:/)) {
1224 var kv = s.split(":");
1225 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1228 // what ever is left... we allow.
1231 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1232 if (!nstyle.length) {
1233 node.removeAttribute('style');
1236 this.iterateChildren(node, this.cleanWord);
1242 * iterateChildren of a Node, calling fn each time, using this as the scole..
1243 * @param {DomNode} node node to iterate children of.
1244 * @param {Function} fn method of this class to call on each item.
1246 iterateChildren : function(node, fn)
1248 if (!node.childNodes.length) {
1251 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1252 fn.call(this, node.childNodes[i])
1260 * Quite often pasting from word etc.. results in tables with column and widths.
1261 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1264 cleanTableWidths : function(node)
1269 this.cleanTableWidths(this.doc.body);
1274 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1277 Roo.log(node.tagName);
1278 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1279 this.iterateChildren(node, this.cleanTableWidths);
1282 if (node.hasAttribute('width')) {
1283 node.removeAttribute('width');
1287 if (node.hasAttribute("style")) {
1290 var styles = node.getAttribute("style").split(";");
1292 Roo.each(styles, function(s) {
1293 if (!s.match(/:/)) {
1296 var kv = s.split(":");
1297 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1300 // what ever is left... we allow.
1303 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1304 if (!nstyle.length) {
1305 node.removeAttribute('style');
1309 this.iterateChildren(node, this.cleanTableWidths);
1317 domToHTML : function(currentElement, depth, nopadtext) {
1320 nopadtext = nopadtext || false;
1322 if (!currentElement) {
1323 return this.domToHTML(this.doc.body);
1326 //Roo.log(currentElement);
1328 var allText = false;
1329 var nodeName = currentElement.nodeName;
1330 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1332 if (nodeName == '#text') {
1334 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1339 if (nodeName != 'BODY') {
1342 // Prints the node tagName, such as <A>, <IMG>, etc
1345 for(i = 0; i < currentElement.attributes.length;i++) {
1347 var aname = currentElement.attributes.item(i).name;
1348 if (!currentElement.attributes.item(i).value.length) {
1351 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1354 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1363 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1366 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1371 // Traverse the tree
1373 var currentElementChild = currentElement.childNodes.item(i);
1377 while (currentElementChild) {
1378 // Formatting code (indent the tree so it looks nice on the screen)
1379 var nopad = nopadtext;
1380 if (lastnode == 'SPAN') {
1384 if (currentElementChild.nodeName == '#text') {
1385 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1386 toadd = nopadtext ? toadd : toadd.trim();
1387 if (!nopad && toadd.length > 80) {
1388 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1393 currentElementChild = currentElement.childNodes.item(i);
1399 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1401 // Recursively traverse the tree structure of the child node
1402 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1403 lastnode = currentElementChild.nodeName;
1405 currentElementChild=currentElement.childNodes.item(i);
1411 // The remaining code is mostly for formatting the tree
1412 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1417 ret+= "</"+tagName+">";
1423 applyBlacklists : function()
1425 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1426 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1430 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1431 if (b.indexOf(tag) > -1) {
1434 this.white.push(tag);
1438 Roo.each(w, function(tag) {
1439 if (b.indexOf(tag) > -1) {
1442 if (this.white.indexOf(tag) > -1) {
1445 this.white.push(tag);
1450 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1451 if (w.indexOf(tag) > -1) {
1454 this.black.push(tag);
1458 Roo.each(b, function(tag) {
1459 if (w.indexOf(tag) > -1) {
1462 if (this.black.indexOf(tag) > -1) {
1465 this.black.push(tag);
1470 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1471 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1475 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1476 if (b.indexOf(tag) > -1) {
1479 this.cwhite.push(tag);
1483 Roo.each(w, function(tag) {
1484 if (b.indexOf(tag) > -1) {
1487 if (this.cwhite.indexOf(tag) > -1) {
1490 this.cwhite.push(tag);
1495 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1496 if (w.indexOf(tag) > -1) {
1499 this.cblack.push(tag);
1503 Roo.each(b, function(tag) {
1504 if (w.indexOf(tag) > -1) {
1507 if (this.cblack.indexOf(tag) > -1) {
1510 this.cblack.push(tag);
1515 setStylesheets : function(stylesheets)
1517 if(typeof(stylesheets) == 'string'){
1518 Roo.get(this.iframe.contentDocument.head).createChild({
1529 Roo.each(stylesheets, function(s) {
1534 Roo.get(_this.iframe.contentDocument.head).createChild({
1545 removeStylesheets : function()
1549 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1554 // hide stuff that is not compatible
1572 * @cfg {String} fieldClass @hide
1575 * @cfg {String} focusClass @hide
1578 * @cfg {String} autoCreate @hide
1581 * @cfg {String} inputType @hide
1584 * @cfg {String} invalidClass @hide
1587 * @cfg {String} invalidText @hide
1590 * @cfg {String} msgFx @hide
1593 * @cfg {String} validateOnBlur @hide
1597 Roo.HtmlEditorCore.white = [
1598 'area', 'br', 'img', 'input', 'hr', 'wbr',
1600 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1601 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1602 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1603 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1604 'table', 'ul', 'xmp',
1606 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1609 'dir', 'menu', 'ol', 'ul', 'dl',
1615 Roo.HtmlEditorCore.black = [
1616 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1618 'base', 'basefont', 'bgsound', 'blink', 'body',
1619 'frame', 'frameset', 'head', 'html', 'ilayer',
1620 'iframe', 'layer', 'link', 'meta', 'object',
1621 'script', 'style' ,'title', 'xml' // clean later..
1623 Roo.HtmlEditorCore.clean = [
1624 'script', 'style', 'title', 'xml'
1626 Roo.HtmlEditorCore.remove = [
1631 Roo.HtmlEditorCore.ablack = [
1635 Roo.HtmlEditorCore.aclean = [
1636 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1640 Roo.HtmlEditorCore.pwhite= [
1641 'http', 'https', 'mailto'
1644 // white listed style attributes.
1645 Roo.HtmlEditorCore.cwhite= [
1646 // 'text-align', /// default is to allow most things..
1652 // black listed style attributes.
1653 Roo.HtmlEditorCore.cblack= [
1654 // 'font-size' -- this can be set by the project
1658 Roo.HtmlEditorCore.swapCodes =[