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.
118 * @cfg {boolean} allowComments - default false - allow comments in HTML source - by default they are stripped - if you are editing email you may need this.
121 allowComments: false,
125 // private properties
126 validationEvent : false,
130 sourceEditMode : false,
131 onFocus : Roo.emptyFn,
137 // blacklist + whitelisted elements..
144 * Protected method that will not generally be called directly. It
145 * is called when the editor initializes the iframe with HTML contents. Override this method if you
146 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
148 getDocMarkup : function(){
152 // inherit styels from page...??
153 if (this.stylesheets === false) {
155 Roo.get(document.head).select('style').each(function(node) {
156 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
159 Roo.get(document.head).select('link').each(function(node) {
160 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
163 } else if (!this.stylesheets.length) {
165 st = '<style type="text/css">' +
166 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
169 for (var i in this.stylesheets) {
170 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
175 st += '<style type="text/css">' +
176 'IMG { cursor: pointer } ' +
179 var cls = 'roo-htmleditor-body';
181 if(this.bodyCls.length){
182 cls += ' ' + this.bodyCls;
185 return '<html><head>' + st +
186 //<style type="text/css">' +
187 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
189 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
193 onRender : function(ct, position)
196 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
197 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
200 this.el.dom.style.border = '0 none';
201 this.el.dom.setAttribute('tabIndex', -1);
202 this.el.addClass('x-hidden hide');
206 if(Roo.isIE){ // fix IE 1px bogus margin
207 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
211 this.frameId = Roo.id();
215 var iframe = this.owner.wrap.createChild({
217 cls: 'form-control', // bootstrap..
221 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
226 this.iframe = iframe.dom;
230 this.doc.designMode = 'on';
233 this.doc.write(this.getDocMarkup());
237 var task = { // must defer to wait for browser to be ready
239 //console.log("run task?" + this.doc.readyState);
241 if(this.doc.body || this.doc.readyState == 'complete'){
243 this.doc.designMode="on";
247 Roo.TaskMgr.stop(task);
248 this.initEditor.defer(10, this);
255 Roo.TaskMgr.start(task);
260 onResize : function(w, h)
262 Roo.log('resize: ' +w + ',' + h );
263 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
267 if(typeof w == 'number'){
269 this.iframe.style.width = w + 'px';
271 if(typeof h == 'number'){
273 this.iframe.style.height = h + 'px';
275 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
282 * Toggles the editor between standard and source edit mode.
283 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
285 toggleSourceEdit : function(sourceEditMode){
287 this.sourceEditMode = sourceEditMode === true;
289 if(this.sourceEditMode){
291 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
294 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
295 //this.iframe.className = '';
298 //this.setSize(this.owner.wrap.getSize());
299 //this.fireEvent('editmodechange', this, this.sourceEditMode);
306 * Protected method that will not generally be called directly. If you need/want
307 * custom HTML cleanup, this is the method you should override.
308 * @param {String} html The HTML to be cleaned
309 * return {String} The cleaned HTML
311 cleanHtml : function(html){
314 if(Roo.isSafari){ // strip safari nonsense
315 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
318 if(html == ' '){
325 * HTML Editor -> Textarea
326 * Protected method that will not generally be called directly. Syncs the contents
327 * of the editor iframe with the textarea.
329 syncValue : function(){
330 if(this.initialized){
331 var bd = (this.doc.body || this.doc.documentElement);
332 //this.cleanUpPaste(); -- this is done else where and causes havoc..
333 var html = bd.innerHTML;
335 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
336 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
338 html = '<div style="'+m[0]+'">' + html + '</div>';
341 html = this.cleanHtml(html);
342 // fix up the special chars.. normaly like back quotes in word...
343 // however we do not want to do this with chinese..
344 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
346 var cc = match.charCodeAt();
348 // Get the character value, handling surrogate pairs
349 if (match.length == 2) {
350 // It's a surrogate pair, calculate the Unicode code point
351 var high = match.charCodeAt(0) - 0xD800;
352 var low = match.charCodeAt(1) - 0xDC00;
353 cc = (high * 0x400) + low + 0x10000;
355 (cc >= 0x4E00 && cc < 0xA000 ) ||
356 (cc >= 0x3400 && cc < 0x4E00 ) ||
357 (cc >= 0xf900 && cc < 0xfb00 )
362 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
363 return "&#" + cc + ";";
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' ||
554 tg.toLowerCase() == 'code' ||
555 tg.toLowerCase() == 'sup' ||
556 tg.toLowerCase() == 'sub'
559 range = this.createRange(this.getSelection());
560 var wrappingNode = this.doc.createElement(tg.toLowerCase());
561 wrappingNode.appendChild(range.extractContents());
562 range.insertNode(wrappingNode);
569 this.execCmd("formatblock", tg);
573 insertText : function(txt)
577 var range = this.createRange();
578 range.deleteContents();
579 //alert(Sender.getAttribute('label'));
581 range.insertNode(this.doc.createTextNode(txt));
587 * Executes a Midas editor command on the editor document and performs necessary focus and
588 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
589 * @param {String} cmd The Midas command
590 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
592 relayCmd : function(cmd, value){
594 this.execCmd(cmd, value);
595 this.owner.fireEvent('editorevent', this);
596 //this.updateToolbar();
597 this.owner.deferFocus();
601 * Executes a Midas editor command directly on the editor document.
602 * For visual commands, you should use {@link #relayCmd} instead.
603 * <b>This should only be called after the editor is initialized.</b>
604 * @param {String} cmd The Midas command
605 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
607 execCmd : function(cmd, value){
608 this.doc.execCommand(cmd, false, value === undefined ? null : value);
615 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
617 * @param {String} text | dom node..
619 insertAtCursor : function(text)
628 var r = this.doc.selection.createRange();
639 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
643 // from jquery ui (MIT licenced)
647 if (win.getSelection && win.getSelection().getRangeAt) {
648 range = win.getSelection().getRangeAt(0);
649 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
650 range.insertNode(node);
651 } else if (win.document.selection && win.document.selection.createRange) {
652 // no firefox support
653 var txt = typeof(text) == 'string' ? text : text.outerHTML;
654 win.document.selection.createRange().pasteHTML(txt);
656 // no firefox support
657 var txt = typeof(text) == 'string' ? text : text.outerHTML;
658 this.execCmd('InsertHTML', txt);
667 mozKeyPress : function(e){
669 var c = e.getCharCode(), cmd;
672 c = String.fromCharCode(c).toLowerCase();
686 this.cleanUpPaste.defer(100, this);
702 fixKeys : function(){ // load time branching for fastest keydown performance
705 var k = e.getKey(), r;
708 r = this.doc.selection.createRange();
711 r.pasteHTML('    ');
718 r = this.doc.selection.createRange();
720 var target = r.parentElement();
721 if(!target || target.tagName.toLowerCase() != 'li'){
723 r.pasteHTML('<br />');
729 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
730 this.cleanUpPaste.defer(100, this);
736 }else if(Roo.isOpera){
742 this.execCmd('InsertHTML','    ');
745 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
746 this.cleanUpPaste.defer(100, this);
751 }else if(Roo.isSafari){
757 this.execCmd('InsertText','\t');
761 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
762 this.cleanUpPaste.defer(100, this);
770 getAllAncestors: function()
772 var p = this.getSelectedNode();
775 a.push(p); // push blank onto stack..
776 p = this.getParentElement();
780 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
784 a.push(this.doc.body);
791 getSelection : function()
794 return Roo.isIE ? this.doc.selection : this.win.getSelection();
797 getSelectedNode: function()
799 // this may only work on Gecko!!!
801 // should we cache this!!!!
806 var range = this.createRange(this.getSelection()).cloneRange();
809 var parent = range.parentElement();
811 var testRange = range.duplicate();
812 testRange.moveToElementText(parent);
813 if (testRange.inRange(range)) {
816 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
819 parent = parent.parentElement;
824 // is ancestor a text element.
825 var ac = range.commonAncestorContainer;
826 if (ac.nodeType == 3) {
830 var ar = ac.childNodes;
833 var other_nodes = [];
834 var has_other_nodes = false;
835 for (var i=0;i<ar.length;i++) {
836 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
839 // fullly contained node.
841 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
846 // probably selected..
847 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
848 other_nodes.push(ar[i]);
852 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
857 has_other_nodes = true;
859 if (!nodes.length && other_nodes.length) {
862 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
868 createRange: function(sel)
870 // this has strange effects when using with
871 // top toolbar - not sure if it's a great idea.
872 //this.editor.contentWindow.focus();
873 if (typeof sel != "undefined") {
875 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
877 return this.doc.createRange();
880 return this.doc.createRange();
883 getParentElement: function()
887 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
889 var range = this.createRange(sel);
892 var p = range.commonAncestorContainer;
893 while (p.nodeType == 3) { // text node
904 * Range intersection.. the hard stuff...
908 * [ -- selected range --- ]
912 * if end is before start or hits it. fail.
913 * if start is after end or hits it fail.
915 * if either hits (but other is outside. - then it's not
921 // @see http://www.thismuchiknow.co.uk/?p=64.
922 rangeIntersectsNode : function(range, node)
924 var nodeRange = node.ownerDocument.createRange();
926 nodeRange.selectNode(node);
928 nodeRange.selectNodeContents(node);
931 var rangeStartRange = range.cloneRange();
932 rangeStartRange.collapse(true);
934 var rangeEndRange = range.cloneRange();
935 rangeEndRange.collapse(false);
937 var nodeStartRange = nodeRange.cloneRange();
938 nodeStartRange.collapse(true);
940 var nodeEndRange = nodeRange.cloneRange();
941 nodeEndRange.collapse(false);
943 return rangeStartRange.compareBoundaryPoints(
944 Range.START_TO_START, nodeEndRange) == -1 &&
945 rangeEndRange.compareBoundaryPoints(
946 Range.START_TO_START, nodeStartRange) == 1;
950 rangeCompareNode : function(range, node)
952 var nodeRange = node.ownerDocument.createRange();
954 nodeRange.selectNode(node);
956 nodeRange.selectNodeContents(node);
960 range.collapse(true);
962 nodeRange.collapse(true);
964 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
965 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
967 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
969 var nodeIsBefore = ss == 1;
970 var nodeIsAfter = ee == -1;
972 if (nodeIsBefore && nodeIsAfter) {
975 if (!nodeIsBefore && nodeIsAfter) {
976 return 1; //right trailed.
979 if (nodeIsBefore && !nodeIsAfter) {
980 return 2; // left trailed.
986 // private? - in a new class?
987 cleanUpPaste : function()
989 // cleans up the whole document..
990 Roo.log('cleanuppaste');
992 this.cleanUpChildren(this.doc.body);
993 var clean = this.cleanWordChars(this.doc.body.innerHTML);
994 if (clean != this.doc.body.innerHTML) {
995 this.doc.body.innerHTML = clean;
1000 cleanWordChars : function(input) {// change the chars to hex code
1001 var he = Roo.HtmlEditorCore;
1004 Roo.each(he.swapCodes, function(sw) {
1005 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1007 output = output.replace(swapper, sw[1]);
1014 cleanUpChildren : function (n)
1016 if (!n.childNodes.length) {
1019 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1020 this.cleanUpChild(n.childNodes[i]);
1027 cleanUpChild : function (node)
1030 //console.log(node);
1031 if (node.nodeName == "#text") {
1032 // clean up silly Windows -- stuff?
1035 if (node.nodeName == "#comment" && !this.allowComments) {
1036 node.parentNode.removeChild(node);
1037 // clean up silly Windows -- stuff?
1040 var lcname = node.tagName.toLowerCase();
1041 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1042 // whitelist of tags..
1044 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1046 node.parentNode.removeChild(node);
1051 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1053 // spans with no attributes - just remove them..
1054 if ((!node.attributes || !node.attributes.length) && lcname == 'span') {
1055 remove_keep_children = true;
1058 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1059 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1061 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1062 // remove_keep_children = true;
1065 if (remove_keep_children) {
1066 this.cleanUpChildren(node);
1067 // inserts everything just before this node...
1068 while (node.childNodes.length) {
1069 var cn = node.childNodes[0];
1070 node.removeChild(cn);
1071 node.parentNode.insertBefore(cn, node);
1073 node.parentNode.removeChild(node);
1077 if (!node.attributes || !node.attributes.length) {
1082 this.cleanUpChildren(node);
1086 function cleanAttr(n,v)
1089 if (v.match(/^\./) || v.match(/^\//)) {
1092 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1095 if (v.match(/^#/)) {
1098 if (v.match(/^\{/)) { // allow template editing.
1101 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1102 node.removeAttribute(n);
1106 var cwhite = this.cwhite;
1107 var cblack = this.cblack;
1109 function cleanStyle(n,v)
1111 if (v.match(/expression/)) { //XSS?? should we even bother..
1112 node.removeAttribute(n);
1116 var parts = v.split(/;/);
1119 Roo.each(parts, function(p) {
1120 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1124 var l = p.split(':').shift().replace(/\s+/g,'');
1125 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1127 if ( cwhite.length && cblack.indexOf(l) > -1) {
1128 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1129 //node.removeAttribute(n);
1133 // only allow 'c whitelisted system attributes'
1134 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1135 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1136 //node.removeAttribute(n);
1147 node.setAttribute(n, clean.join(';'));
1149 node.removeAttribute(n);
1155 for (var i = node.attributes.length-1; i > -1 ; i--) {
1156 var a = node.attributes[i];
1159 if (a.name.toLowerCase().substr(0,2)=='on') {
1160 node.removeAttribute(a.name);
1163 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1164 node.removeAttribute(a.name);
1167 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1168 cleanAttr(a.name,a.value); // fixme..
1171 if (a.name == 'style') {
1172 cleanStyle(a.name,a.value);
1175 /// clean up MS crap..
1176 // tecnically this should be a list of valid class'es..
1179 if (a.name == 'class') {
1180 if (a.value.match(/^Mso/)) {
1181 node.removeAttribute('class');
1184 if (a.value.match(/^body$/)) {
1185 node.removeAttribute('class');
1196 this.cleanUpChildren(node);
1202 * Clean up MS wordisms...
1204 cleanWord : function(node)
1207 this.cleanWord(this.doc.body);
1212 node.nodeName == 'SPAN' &&
1213 !node.hasAttributes() &&
1214 node.childNodes.length == 1 &&
1215 node.firstChild.nodeName == "#text"
1217 var textNode = node.firstChild;
1218 node.removeChild(textNode);
1219 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1220 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1222 node.parentNode.insertBefore(textNode, node);
1223 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1224 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1226 node.parentNode.removeChild(node);
1229 if (node.nodeName == "#text") {
1230 // clean up silly Windows -- stuff?
1233 if (node.nodeName == "#comment") {
1234 node.parentNode.removeChild(node);
1235 // clean up silly Windows -- stuff?
1239 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1240 node.parentNode.removeChild(node);
1243 //Roo.log(node.tagName);
1244 // remove - but keep children..
1245 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|v:|font)/)) {
1246 //Roo.log('-- removed');
1247 while (node.childNodes.length) {
1248 var cn = node.childNodes[0];
1249 node.removeChild(cn);
1250 node.parentNode.insertBefore(cn, node);
1251 // move node to parent - and clean it..
1254 node.parentNode.removeChild(node);
1255 /// no need to iterate chidlren = it's got none..
1256 //this.iterateChildren(node, this.cleanWord);
1260 if (node.className.length) {
1262 var cn = node.className.split(/\W+/);
1264 Roo.each(cn, function(cls) {
1265 if (cls.match(/Mso[a-zA-Z]+/)) {
1270 node.className = cna.length ? cna.join(' ') : '';
1272 node.removeAttribute("class");
1276 if (node.hasAttribute("lang")) {
1277 node.removeAttribute("lang");
1280 if (node.hasAttribute("style")) {
1282 var styles = node.getAttribute("style").split(";");
1284 Roo.each(styles, function(s) {
1285 if (!s.match(/:/)) {
1288 var kv = s.split(":");
1289 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1292 // what ever is left... we allow.
1295 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1296 if (!nstyle.length) {
1297 node.removeAttribute('style');
1300 this.iterateChildren(node, this.cleanWord);
1306 * iterateChildren of a Node, calling fn each time, using this as the scole..
1307 * @param {DomNode} node node to iterate children of.
1308 * @param {Function} fn method of this class to call on each item.
1310 iterateChildren : function(node, fn)
1312 if (!node.childNodes.length) {
1315 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1316 fn.call(this, node.childNodes[i])
1324 * Quite often pasting from word etc.. results in tables with column and widths.
1325 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1328 cleanTableWidths : function(node)
1333 this.cleanTableWidths(this.doc.body);
1338 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1341 Roo.log(node.tagName);
1342 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1343 this.iterateChildren(node, this.cleanTableWidths);
1346 if (node.hasAttribute('width')) {
1347 node.removeAttribute('width');
1351 if (node.hasAttribute("style")) {
1354 var styles = node.getAttribute("style").split(";");
1356 Roo.each(styles, function(s) {
1357 if (!s.match(/:/)) {
1360 var kv = s.split(":");
1361 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1364 // what ever is left... we allow.
1367 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1368 if (!nstyle.length) {
1369 node.removeAttribute('style');
1373 this.iterateChildren(node, this.cleanTableWidths);
1381 domToHTML : function(currentElement, depth, nopadtext) {
1384 nopadtext = nopadtext || false;
1386 if (!currentElement) {
1387 return this.domToHTML(this.doc.body);
1390 //Roo.log(currentElement);
1392 var allText = false;
1393 var nodeName = currentElement.nodeName;
1394 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1396 if (nodeName == '#text') {
1398 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1403 if (nodeName != 'BODY') {
1406 // Prints the node tagName, such as <A>, <IMG>, etc
1409 for(i = 0; i < currentElement.attributes.length;i++) {
1411 var aname = currentElement.attributes.item(i).name;
1412 if (!currentElement.attributes.item(i).value.length) {
1415 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1418 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1427 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1430 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1435 // Traverse the tree
1437 var currentElementChild = currentElement.childNodes.item(i);
1441 while (currentElementChild) {
1442 // Formatting code (indent the tree so it looks nice on the screen)
1443 var nopad = nopadtext;
1444 if (lastnode == 'SPAN') {
1448 if (currentElementChild.nodeName == '#text') {
1449 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1450 toadd = nopadtext ? toadd : toadd.trim();
1451 if (!nopad && toadd.length > 80) {
1452 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1457 currentElementChild = currentElement.childNodes.item(i);
1463 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1465 // Recursively traverse the tree structure of the child node
1466 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1467 lastnode = currentElementChild.nodeName;
1469 currentElementChild=currentElement.childNodes.item(i);
1475 // The remaining code is mostly for formatting the tree
1476 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1481 ret+= "</"+tagName+">";
1487 applyBlacklists : function()
1489 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1490 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1494 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1495 if (b.indexOf(tag) > -1) {
1498 this.white.push(tag);
1502 Roo.each(w, function(tag) {
1503 if (b.indexOf(tag) > -1) {
1506 if (this.white.indexOf(tag) > -1) {
1509 this.white.push(tag);
1514 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1515 if (w.indexOf(tag) > -1) {
1518 this.black.push(tag);
1522 Roo.each(b, function(tag) {
1523 if (w.indexOf(tag) > -1) {
1526 if (this.black.indexOf(tag) > -1) {
1529 this.black.push(tag);
1534 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1535 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1539 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1540 if (b.indexOf(tag) > -1) {
1543 this.cwhite.push(tag);
1547 Roo.each(w, function(tag) {
1548 if (b.indexOf(tag) > -1) {
1551 if (this.cwhite.indexOf(tag) > -1) {
1554 this.cwhite.push(tag);
1559 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1560 if (w.indexOf(tag) > -1) {
1563 this.cblack.push(tag);
1567 Roo.each(b, function(tag) {
1568 if (w.indexOf(tag) > -1) {
1571 if (this.cblack.indexOf(tag) > -1) {
1574 this.cblack.push(tag);
1579 setStylesheets : function(stylesheets)
1581 if(typeof(stylesheets) == 'string'){
1582 Roo.get(this.iframe.contentDocument.head).createChild({
1593 Roo.each(stylesheets, function(s) {
1598 Roo.get(_this.iframe.contentDocument.head).createChild({
1609 removeStylesheets : function()
1613 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1618 setStyle : function(style)
1620 Roo.get(this.iframe.contentDocument.head).createChild({
1629 // hide stuff that is not compatible
1647 * @cfg {String} fieldClass @hide
1650 * @cfg {String} focusClass @hide
1653 * @cfg {String} autoCreate @hide
1656 * @cfg {String} inputType @hide
1659 * @cfg {String} invalidClass @hide
1662 * @cfg {String} invalidText @hide
1665 * @cfg {String} msgFx @hide
1668 * @cfg {String} validateOnBlur @hide
1672 Roo.HtmlEditorCore.white = [
1673 'area', 'br', 'img', 'input', 'hr', 'wbr',
1675 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1676 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1677 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1678 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1679 'table', 'ul', 'xmp',
1681 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1684 'dir', 'menu', 'ol', 'ul', 'dl',
1690 Roo.HtmlEditorCore.black = [
1691 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1693 'base', 'basefont', 'bgsound', 'blink', 'body',
1694 'frame', 'frameset', 'head', 'html', 'ilayer',
1695 'iframe', 'layer', 'link', 'meta', 'object',
1696 'script', 'style' ,'title', 'xml' // clean later..
1698 Roo.HtmlEditorCore.clean = [
1699 'script', 'style', 'title', 'xml'
1701 Roo.HtmlEditorCore.remove = [
1706 Roo.HtmlEditorCore.ablack = [
1710 Roo.HtmlEditorCore.aclean = [
1711 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1715 Roo.HtmlEditorCore.pwhite= [
1716 'http', 'https', 'mailto'
1719 // white listed style attributes.
1720 Roo.HtmlEditorCore.cwhite= [
1721 // 'text-align', /// default is to allow most things..
1727 // black listed style attributes.
1728 Roo.HtmlEditorCore.cblack= [
1729 // 'font-size' -- this can be set by the project
1733 Roo.HtmlEditorCore.swapCodes =[
1734 [ 8211, "–" ],
1735 [ 8212, "—" ],