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.
120 allowComments: false,
124 // private properties
125 validationEvent : false,
129 sourceEditMode : false,
130 onFocus : Roo.emptyFn,
136 // blacklist + whitelisted elements..
143 * Protected method that will not generally be called directly. It
144 * is called when the editor initializes the iframe with HTML contents. Override this method if you
145 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
147 getDocMarkup : function(){
151 // inherit styels from page...??
152 if (this.stylesheets === false) {
154 Roo.get(document.head).select('style').each(function(node) {
155 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
158 Roo.get(document.head).select('link').each(function(node) {
159 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
162 } else if (!this.stylesheets.length) {
164 st = '<style type="text/css">' +
165 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
168 for (var i in this.stylesheets) {
169 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
174 st += '<style type="text/css">' +
175 'IMG { cursor: pointer } ' +
178 var cls = 'roo-htmleditor-body';
180 if(this.bodyCls.length){
181 cls += ' ' + this.bodyCls;
184 return '<html><head>' + st +
185 //<style type="text/css">' +
186 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
188 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
192 onRender : function(ct, position)
195 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
196 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
199 this.el.dom.style.border = '0 none';
200 this.el.dom.setAttribute('tabIndex', -1);
201 this.el.addClass('x-hidden hide');
205 if(Roo.isIE){ // fix IE 1px bogus margin
206 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
210 this.frameId = Roo.id();
214 var iframe = this.owner.wrap.createChild({
216 cls: 'form-control', // bootstrap..
220 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
225 this.iframe = iframe.dom;
229 this.doc.designMode = 'on';
232 this.doc.write(this.getDocMarkup());
236 var task = { // must defer to wait for browser to be ready
238 //console.log("run task?" + this.doc.readyState);
240 if(this.doc.body || this.doc.readyState == 'complete'){
242 this.doc.designMode="on";
246 Roo.TaskMgr.stop(task);
247 this.initEditor.defer(10, this);
254 Roo.TaskMgr.start(task);
259 onResize : function(w, h)
261 Roo.log('resize: ' +w + ',' + h );
262 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
266 if(typeof w == 'number'){
268 this.iframe.style.width = w + 'px';
270 if(typeof h == 'number'){
272 this.iframe.style.height = h + 'px';
274 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
281 * Toggles the editor between standard and source edit mode.
282 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
284 toggleSourceEdit : function(sourceEditMode){
286 this.sourceEditMode = sourceEditMode === true;
288 if(this.sourceEditMode){
290 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
293 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
294 //this.iframe.className = '';
297 //this.setSize(this.owner.wrap.getSize());
298 //this.fireEvent('editmodechange', this, this.sourceEditMode);
305 * Protected method that will not generally be called directly. If you need/want
306 * custom HTML cleanup, this is the method you should override.
307 * @param {String} html The HTML to be cleaned
308 * return {String} The cleaned HTML
310 cleanHtml : function(html){
313 if(Roo.isSafari){ // strip safari nonsense
314 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
317 if(html == ' '){
324 * HTML Editor -> Textarea
325 * Protected method that will not generally be called directly. Syncs the contents
326 * of the editor iframe with the textarea.
328 syncValue : function(){
329 if(this.initialized){
330 var bd = (this.doc.body || this.doc.documentElement);
331 //this.cleanUpPaste(); -- this is done else where and causes havoc..
332 var html = bd.innerHTML;
334 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
335 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
337 html = '<div style="'+m[0]+'">' + html + '</div>';
340 html = this.cleanHtml(html);
341 // fix up the special chars.. normaly like back quotes in word...
342 // however we do not want to do this with chinese..
343 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
345 var cc = match.charCodeAt();
347 // Get the character value, handling surrogate pairs
348 if (match.length == 2) {
349 // It's a surrogate pair, calculate the Unicode code point
350 var high = match.charCodeAt(0) - 0xD800;
351 var low = match.charCodeAt(1) - 0xDC00;
352 cc = (high * 0x400) + low + 0x10000;
354 (cc >= 0x4E00 && cc < 0xA000 ) ||
355 (cc >= 0x3400 && cc < 0x4E00 ) ||
356 (cc >= 0xf900 && cc < 0xfb00 )
361 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
362 return "&#" + cc + ";";
369 if(this.owner.fireEvent('beforesync', this, html) !== false){
370 this.el.dom.value = html;
371 this.owner.fireEvent('sync', this, html);
377 * Protected method that will not generally be called directly. Pushes the value of the textarea
378 * into the iframe editor.
380 pushValue : function(){
381 if(this.initialized){
382 var v = this.el.dom.value.trim();
388 if(this.owner.fireEvent('beforepush', this, v) !== false){
389 var d = (this.doc.body || this.doc.documentElement);
392 this.el.dom.value = d.innerHTML;
393 this.owner.fireEvent('push', this, v);
399 deferFocus : function(){
400 this.focus.defer(10, this);
405 if(this.win && !this.sourceEditMode){
412 assignDocWin: function()
414 var iframe = this.iframe;
417 this.doc = iframe.contentWindow.document;
418 this.win = iframe.contentWindow;
420 // if (!Roo.get(this.frameId)) {
423 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
424 // this.win = Roo.get(this.frameId).dom.contentWindow;
426 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
430 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
431 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
436 initEditor : function(){
437 //console.log("INIT EDITOR");
442 this.doc.designMode="on";
444 this.doc.write(this.getDocMarkup());
447 var dbody = (this.doc.body || this.doc.documentElement);
448 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
449 // this copies styles from the containing element into thsi one..
450 // not sure why we need all of this..
451 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
453 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
454 //ss['background-attachment'] = 'fixed'; // w3c
455 dbody.bgProperties = 'fixed'; // ie
456 //Roo.DomHelper.applyStyles(dbody, ss);
457 Roo.EventManager.on(this.doc, {
458 //'mousedown': this.onEditorEvent,
459 'mouseup': this.onEditorEvent,
460 'dblclick': this.onEditorEvent,
461 'click': this.onEditorEvent,
462 'keyup': this.onEditorEvent,
467 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
469 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
470 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
472 this.initialized = true;
474 this.owner.fireEvent('initialize', this);
479 onDestroy : function(){
485 //for (var i =0; i < this.toolbars.length;i++) {
486 // // fixme - ask toolbars for heights?
487 // this.toolbars[i].onDestroy();
490 //this.wrap.dom.innerHTML = '';
491 //this.wrap.remove();
496 onFirstFocus : function(){
501 this.activated = true;
504 if(Roo.isGecko){ // prevent silly gecko errors
506 var s = this.win.getSelection();
507 if(!s.focusNode || s.focusNode.nodeType != 3){
508 var r = s.getRangeAt(0);
509 r.selectNodeContents((this.doc.body || this.doc.documentElement));
514 this.execCmd('useCSS', true);
515 this.execCmd('styleWithCSS', false);
518 this.owner.fireEvent('activate', this);
522 adjustFont: function(btn){
523 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
524 //if(Roo.isSafari){ // safari
527 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
528 if(Roo.isSafari){ // safari
529 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
530 v = (v < 10) ? 10 : v;
531 v = (v > 48) ? 48 : v;
532 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
537 v = Math.max(1, v+adjust);
539 this.execCmd('FontSize', v );
542 onEditorEvent : function(e)
544 this.owner.fireEvent('editorevent', this, e);
545 // this.updateToolbar();
546 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
549 insertTag : function(tg)
551 // could be a bit smarter... -> wrap the current selected tRoo..
552 if (tg.toLowerCase() == 'span' ||
553 tg.toLowerCase() == 'code' ||
554 tg.toLowerCase() == 'sup' ||
555 tg.toLowerCase() == 'sub'
558 range = this.createRange(this.getSelection());
559 var wrappingNode = this.doc.createElement(tg.toLowerCase());
560 wrappingNode.appendChild(range.extractContents());
561 range.insertNode(wrappingNode);
568 this.execCmd("formatblock", tg);
572 insertText : function(txt)
576 var range = this.createRange();
577 range.deleteContents();
578 //alert(Sender.getAttribute('label'));
580 range.insertNode(this.doc.createTextNode(txt));
586 * Executes a Midas editor command on the editor document and performs necessary focus and
587 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
588 * @param {String} cmd The Midas command
589 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
591 relayCmd : function(cmd, value){
593 this.execCmd(cmd, value);
594 this.owner.fireEvent('editorevent', this);
595 //this.updateToolbar();
596 this.owner.deferFocus();
600 * Executes a Midas editor command directly on the editor document.
601 * For visual commands, you should use {@link #relayCmd} instead.
602 * <b>This should only be called after the editor is initialized.</b>
603 * @param {String} cmd The Midas command
604 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
606 execCmd : function(cmd, value){
607 this.doc.execCommand(cmd, false, value === undefined ? null : value);
614 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
616 * @param {String} text | dom node..
618 insertAtCursor : function(text)
627 var r = this.doc.selection.createRange();
638 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
642 // from jquery ui (MIT licenced)
646 if (win.getSelection && win.getSelection().getRangeAt) {
647 range = win.getSelection().getRangeAt(0);
648 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
649 range.insertNode(node);
650 } else if (win.document.selection && win.document.selection.createRange) {
651 // no firefox support
652 var txt = typeof(text) == 'string' ? text : text.outerHTML;
653 win.document.selection.createRange().pasteHTML(txt);
655 // no firefox support
656 var txt = typeof(text) == 'string' ? text : text.outerHTML;
657 this.execCmd('InsertHTML', txt);
666 mozKeyPress : function(e){
668 var c = e.getCharCode(), cmd;
671 c = String.fromCharCode(c).toLowerCase();
685 this.cleanUpPaste.defer(100, this);
701 fixKeys : function(){ // load time branching for fastest keydown performance
704 var k = e.getKey(), r;
707 r = this.doc.selection.createRange();
710 r.pasteHTML('    ');
717 r = this.doc.selection.createRange();
719 var target = r.parentElement();
720 if(!target || target.tagName.toLowerCase() != 'li'){
722 r.pasteHTML('<br />');
728 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
729 this.cleanUpPaste.defer(100, this);
735 }else if(Roo.isOpera){
741 this.execCmd('InsertHTML','    ');
744 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
745 this.cleanUpPaste.defer(100, this);
750 }else if(Roo.isSafari){
756 this.execCmd('InsertText','\t');
760 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
761 this.cleanUpPaste.defer(100, this);
769 getAllAncestors: function()
771 var p = this.getSelectedNode();
774 a.push(p); // push blank onto stack..
775 p = this.getParentElement();
779 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
783 a.push(this.doc.body);
790 getSelection : function()
793 return Roo.isIE ? this.doc.selection : this.win.getSelection();
796 getSelectedNode: function()
798 // this may only work on Gecko!!!
800 // should we cache this!!!!
805 var range = this.createRange(this.getSelection()).cloneRange();
808 var parent = range.parentElement();
810 var testRange = range.duplicate();
811 testRange.moveToElementText(parent);
812 if (testRange.inRange(range)) {
815 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
818 parent = parent.parentElement;
823 // is ancestor a text element.
824 var ac = range.commonAncestorContainer;
825 if (ac.nodeType == 3) {
829 var ar = ac.childNodes;
832 var other_nodes = [];
833 var has_other_nodes = false;
834 for (var i=0;i<ar.length;i++) {
835 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
838 // fullly contained node.
840 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
845 // probably selected..
846 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
847 other_nodes.push(ar[i]);
851 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
856 has_other_nodes = true;
858 if (!nodes.length && other_nodes.length) {
861 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
867 createRange: function(sel)
869 // this has strange effects when using with
870 // top toolbar - not sure if it's a great idea.
871 //this.editor.contentWindow.focus();
872 if (typeof sel != "undefined") {
874 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
876 return this.doc.createRange();
879 return this.doc.createRange();
882 getParentElement: function()
886 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
888 var range = this.createRange(sel);
891 var p = range.commonAncestorContainer;
892 while (p.nodeType == 3) { // text node
903 * Range intersection.. the hard stuff...
907 * [ -- selected range --- ]
911 * if end is before start or hits it. fail.
912 * if start is after end or hits it fail.
914 * if either hits (but other is outside. - then it's not
920 // @see http://www.thismuchiknow.co.uk/?p=64.
921 rangeIntersectsNode : function(range, node)
923 var nodeRange = node.ownerDocument.createRange();
925 nodeRange.selectNode(node);
927 nodeRange.selectNodeContents(node);
930 var rangeStartRange = range.cloneRange();
931 rangeStartRange.collapse(true);
933 var rangeEndRange = range.cloneRange();
934 rangeEndRange.collapse(false);
936 var nodeStartRange = nodeRange.cloneRange();
937 nodeStartRange.collapse(true);
939 var nodeEndRange = nodeRange.cloneRange();
940 nodeEndRange.collapse(false);
942 return rangeStartRange.compareBoundaryPoints(
943 Range.START_TO_START, nodeEndRange) == -1 &&
944 rangeEndRange.compareBoundaryPoints(
945 Range.START_TO_START, nodeStartRange) == 1;
949 rangeCompareNode : function(range, node)
951 var nodeRange = node.ownerDocument.createRange();
953 nodeRange.selectNode(node);
955 nodeRange.selectNodeContents(node);
959 range.collapse(true);
961 nodeRange.collapse(true);
963 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
964 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
966 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
968 var nodeIsBefore = ss == 1;
969 var nodeIsAfter = ee == -1;
971 if (nodeIsBefore && nodeIsAfter) {
974 if (!nodeIsBefore && nodeIsAfter) {
975 return 1; //right trailed.
978 if (nodeIsBefore && !nodeIsAfter) {
979 return 2; // left trailed.
985 // private? - in a new class?
986 cleanUpPaste : function()
988 // cleans up the whole document..
989 Roo.log('cleanuppaste');
991 this.cleanUpChildren(this.doc.body);
992 var clean = this.cleanWordChars(this.doc.body.innerHTML);
993 if (clean != this.doc.body.innerHTML) {
994 this.doc.body.innerHTML = clean;
999 cleanWordChars : function(input) {// change the chars to hex code
1000 var he = Roo.HtmlEditorCore;
1003 Roo.each(he.swapCodes, function(sw) {
1004 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1006 output = output.replace(swapper, sw[1]);
1013 cleanUpChildren : function (n)
1015 if (!n.childNodes.length) {
1018 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1019 this.cleanUpChild(n.childNodes[i]);
1026 cleanUpChild : function (node)
1029 //console.log(node);
1030 if (node.nodeName == "#text") {
1031 // clean up silly Windows -- stuff?
1034 if (node.nodeName == "#comment") {
1035 if (!this.allowComments) {
1036 node.parentNode.removeChild(node);
1038 // clean up silly Windows -- stuff?
1041 var lcname = node.tagName.toLowerCase();
1042 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1043 // whitelist of tags..
1045 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1047 node.parentNode.removeChild(node);
1052 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1054 // spans with no attributes - just remove them..
1055 if ((!node.attributes || !node.attributes.length) && lcname == 'span') {
1056 remove_keep_children = true;
1059 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1060 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1062 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1063 // remove_keep_children = true;
1066 if (remove_keep_children) {
1067 this.cleanUpChildren(node);
1068 // inserts everything just before this node...
1069 while (node.childNodes.length) {
1070 var cn = node.childNodes[0];
1071 node.removeChild(cn);
1072 node.parentNode.insertBefore(cn, node);
1074 node.parentNode.removeChild(node);
1078 if (!node.attributes || !node.attributes.length) {
1083 this.cleanUpChildren(node);
1087 function cleanAttr(n,v)
1090 if (v.match(/^\./) || v.match(/^\//)) {
1093 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1096 if (v.match(/^#/)) {
1099 if (v.match(/^\{/)) { // allow template editing.
1102 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1103 node.removeAttribute(n);
1107 var cwhite = this.cwhite;
1108 var cblack = this.cblack;
1110 function cleanStyle(n,v)
1112 if (v.match(/expression/)) { //XSS?? should we even bother..
1113 node.removeAttribute(n);
1117 var parts = v.split(/;/);
1120 Roo.each(parts, function(p) {
1121 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1125 var l = p.split(':').shift().replace(/\s+/g,'');
1126 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1128 if ( cwhite.length && cblack.indexOf(l) > -1) {
1129 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1130 //node.removeAttribute(n);
1134 // only allow 'c whitelisted system attributes'
1135 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1136 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1137 //node.removeAttribute(n);
1148 node.setAttribute(n, clean.join(';'));
1150 node.removeAttribute(n);
1156 for (var i = node.attributes.length-1; i > -1 ; i--) {
1157 var a = node.attributes[i];
1160 if (a.name.toLowerCase().substr(0,2)=='on') {
1161 node.removeAttribute(a.name);
1164 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1165 node.removeAttribute(a.name);
1168 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1169 cleanAttr(a.name,a.value); // fixme..
1172 if (a.name == 'style') {
1173 cleanStyle(a.name,a.value);
1176 /// clean up MS crap..
1177 // tecnically this should be a list of valid class'es..
1180 if (a.name == 'class') {
1181 if (a.value.match(/^Mso/)) {
1182 node.removeAttribute('class');
1185 if (a.value.match(/^body$/)) {
1186 node.removeAttribute('class');
1197 this.cleanUpChildren(node);
1203 * Clean up MS wordisms...
1205 cleanWord : function(node)
1208 this.cleanWord(this.doc.body);
1213 node.nodeName == 'SPAN' &&
1214 !node.hasAttributes() &&
1215 node.childNodes.length == 1 &&
1216 node.firstChild.nodeName == "#text"
1218 var textNode = node.firstChild;
1219 node.removeChild(textNode);
1220 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1221 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1223 node.parentNode.insertBefore(textNode, node);
1224 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1225 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1227 node.parentNode.removeChild(node);
1230 if (node.nodeName == "#text") {
1231 // clean up silly Windows -- stuff?
1234 if (node.nodeName == "#comment") {
1235 node.parentNode.removeChild(node);
1236 // clean up silly Windows -- stuff?
1240 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1241 node.parentNode.removeChild(node);
1244 //Roo.log(node.tagName);
1245 // remove - but keep children..
1246 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|v:|font)/)) {
1247 //Roo.log('-- removed');
1248 while (node.childNodes.length) {
1249 var cn = node.childNodes[0];
1250 node.removeChild(cn);
1251 node.parentNode.insertBefore(cn, node);
1252 // move node to parent - and clean it..
1255 node.parentNode.removeChild(node);
1256 /// no need to iterate chidlren = it's got none..
1257 //this.iterateChildren(node, this.cleanWord);
1261 if (node.className.length) {
1263 var cn = node.className.split(/\W+/);
1265 Roo.each(cn, function(cls) {
1266 if (cls.match(/Mso[a-zA-Z]+/)) {
1271 node.className = cna.length ? cna.join(' ') : '';
1273 node.removeAttribute("class");
1277 if (node.hasAttribute("lang")) {
1278 node.removeAttribute("lang");
1281 if (node.hasAttribute("style")) {
1283 var styles = node.getAttribute("style").split(";");
1285 Roo.each(styles, function(s) {
1286 if (!s.match(/:/)) {
1289 var kv = s.split(":");
1290 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1293 // what ever is left... we allow.
1296 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1297 if (!nstyle.length) {
1298 node.removeAttribute('style');
1301 this.iterateChildren(node, this.cleanWord);
1307 * iterateChildren of a Node, calling fn each time, using this as the scole..
1308 * @param {DomNode} node node to iterate children of.
1309 * @param {Function} fn method of this class to call on each item.
1311 iterateChildren : function(node, fn)
1313 if (!node.childNodes.length) {
1316 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1317 fn.call(this, node.childNodes[i])
1325 * Quite often pasting from word etc.. results in tables with column and widths.
1326 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1329 cleanTableWidths : function(node)
1334 this.cleanTableWidths(this.doc.body);
1339 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1342 Roo.log(node.tagName);
1343 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1344 this.iterateChildren(node, this.cleanTableWidths);
1347 if (node.hasAttribute('width')) {
1348 node.removeAttribute('width');
1352 if (node.hasAttribute("style")) {
1355 var styles = node.getAttribute("style").split(";");
1357 Roo.each(styles, function(s) {
1358 if (!s.match(/:/)) {
1361 var kv = s.split(":");
1362 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1365 // what ever is left... we allow.
1368 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1369 if (!nstyle.length) {
1370 node.removeAttribute('style');
1374 this.iterateChildren(node, this.cleanTableWidths);
1382 domToHTML : function(currentElement, depth, nopadtext) {
1385 nopadtext = nopadtext || false;
1387 if (!currentElement) {
1388 return this.domToHTML(this.doc.body);
1391 //Roo.log(currentElement);
1393 var allText = false;
1394 var nodeName = currentElement.nodeName;
1395 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1397 if (nodeName == '#text') {
1399 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1404 if (nodeName != 'BODY') {
1407 // Prints the node tagName, such as <A>, <IMG>, etc
1410 for(i = 0; i < currentElement.attributes.length;i++) {
1412 var aname = currentElement.attributes.item(i).name;
1413 if (!currentElement.attributes.item(i).value.length) {
1416 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1419 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1428 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1431 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1436 // Traverse the tree
1438 var currentElementChild = currentElement.childNodes.item(i);
1442 while (currentElementChild) {
1443 // Formatting code (indent the tree so it looks nice on the screen)
1444 var nopad = nopadtext;
1445 if (lastnode == 'SPAN') {
1449 if (currentElementChild.nodeName == '#text') {
1450 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1451 toadd = nopadtext ? toadd : toadd.trim();
1452 if (!nopad && toadd.length > 80) {
1453 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1458 currentElementChild = currentElement.childNodes.item(i);
1464 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1466 // Recursively traverse the tree structure of the child node
1467 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1468 lastnode = currentElementChild.nodeName;
1470 currentElementChild=currentElement.childNodes.item(i);
1476 // The remaining code is mostly for formatting the tree
1477 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1482 ret+= "</"+tagName+">";
1488 applyBlacklists : function()
1490 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1491 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1495 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1496 if (b.indexOf(tag) > -1) {
1499 this.white.push(tag);
1503 Roo.each(w, function(tag) {
1504 if (b.indexOf(tag) > -1) {
1507 if (this.white.indexOf(tag) > -1) {
1510 this.white.push(tag);
1515 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1516 if (w.indexOf(tag) > -1) {
1519 this.black.push(tag);
1523 Roo.each(b, function(tag) {
1524 if (w.indexOf(tag) > -1) {
1527 if (this.black.indexOf(tag) > -1) {
1530 this.black.push(tag);
1535 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1536 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1540 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1541 if (b.indexOf(tag) > -1) {
1544 this.cwhite.push(tag);
1548 Roo.each(w, function(tag) {
1549 if (b.indexOf(tag) > -1) {
1552 if (this.cwhite.indexOf(tag) > -1) {
1555 this.cwhite.push(tag);
1560 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1561 if (w.indexOf(tag) > -1) {
1564 this.cblack.push(tag);
1568 Roo.each(b, function(tag) {
1569 if (w.indexOf(tag) > -1) {
1572 if (this.cblack.indexOf(tag) > -1) {
1575 this.cblack.push(tag);
1580 setStylesheets : function(stylesheets)
1582 if(typeof(stylesheets) == 'string'){
1583 Roo.get(this.iframe.contentDocument.head).createChild({
1594 Roo.each(stylesheets, function(s) {
1599 Roo.get(_this.iframe.contentDocument.head).createChild({
1610 removeStylesheets : function()
1614 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1619 setStyle : function(style)
1621 Roo.get(this.iframe.contentDocument.head).createChild({
1630 // hide stuff that is not compatible
1648 * @cfg {String} fieldClass @hide
1651 * @cfg {String} focusClass @hide
1654 * @cfg {String} autoCreate @hide
1657 * @cfg {String} inputType @hide
1660 * @cfg {String} invalidClass @hide
1663 * @cfg {String} invalidText @hide
1666 * @cfg {String} msgFx @hide
1669 * @cfg {String} validateOnBlur @hide
1673 Roo.HtmlEditorCore.white = [
1674 'area', 'br', 'img', 'input', 'hr', 'wbr',
1676 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1677 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1678 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1679 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1680 'table', 'ul', 'xmp',
1682 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1685 'dir', 'menu', 'ol', 'ul', 'dl',
1691 Roo.HtmlEditorCore.black = [
1692 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1694 'base', 'basefont', 'bgsound', 'blink', 'body',
1695 'frame', 'frameset', 'head', 'html', 'ilayer',
1696 'iframe', 'layer', 'link', 'meta', 'object',
1697 'script', 'style' ,'title', 'xml' // clean later..
1699 Roo.HtmlEditorCore.clean = [
1700 'script', 'style', 'title', 'xml'
1702 Roo.HtmlEditorCore.remove = [
1707 Roo.HtmlEditorCore.ablack = [
1711 Roo.HtmlEditorCore.aclean = [
1712 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1716 Roo.HtmlEditorCore.pwhite= [
1717 'http', 'https', 'mailto'
1720 // white listed style attributes.
1721 Roo.HtmlEditorCore.cwhite= [
1722 // 'text-align', /// default is to allow most things..
1728 // black listed style attributes.
1729 Roo.HtmlEditorCore.cblack= [
1730 // 'font-size' -- this can be set by the project
1734 Roo.HtmlEditorCore.swapCodes =[
1735 [ 8211, "–" ],
1736 [ 8212, "—" ],