1 //<script type="text/javascript">
4 * Based Ext JS Library 1.1.1
5 * Copyright(c) 2006-2007, Ext JS, LLC.
11 * @class Roo.HtmlEditorCore
12 * @extends Roo.Component
13 * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
15 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
18 Roo.HtmlEditorCore = function(config){
21 Roo.HtmlEditorCore.superclass.constructor.call(this, config);
27 * Fires when the editor is fully initialized (including the iframe)
28 * @param {Roo.HtmlEditorCore} this
33 * Fires when the editor is first receives the focus. Any insertion must wait
34 * until after this event.
35 * @param {Roo.HtmlEditorCore} this
40 * Fires before the textarea is updated with content from the editor iframe. Return false
42 * @param {Roo.HtmlEditorCore} this
43 * @param {String} html
48 * Fires before the iframe editor is updated with content from the textarea. Return false
50 * @param {Roo.HtmlEditorCore} this
51 * @param {String} html
56 * Fires when the textarea is updated with content from the editor iframe.
57 * @param {Roo.HtmlEditorCore} this
58 * @param {String} html
63 * Fires when the iframe editor is updated with content from the textarea.
64 * @param {Roo.HtmlEditorCore} this
65 * @param {String} html
71 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72 * @param {Roo.HtmlEditorCore} this
78 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
80 // defaults : white / black...
81 this.applyBlacklists();
88 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
92 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
98 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
103 * @cfg {Number} height (in pixels)
107 * @cfg {Number} width (in pixels)
112 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
120 // private properties
121 validationEvent : false,
125 sourceEditMode : false,
126 onFocus : Roo.emptyFn,
132 // blacklist + whitelisted elements..
139 * Protected method that will not generally be called directly. It
140 * is called when the editor initializes the iframe with HTML contents. Override this method if you
141 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
143 getDocMarkup : function(){
147 // inherit styels from page...??
148 if (this.stylesheets === false) {
150 Roo.get(document.head).select('style').each(function(node) {
151 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
154 Roo.get(document.head).select('link').each(function(node) {
155 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
158 } else if (!this.stylesheets.length) {
160 st = '<style type="text/css">' +
161 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
164 st = '<style type="text/css">' +
169 st += '<style type="text/css">' +
170 'IMG { cursor: pointer } ' +
173 var cls = 'roo-htmleditor-body';
175 if(this.bodyCls.length){
176 cls += ' ' + this.bodyCls;
179 return '<html><head>' + st +
180 //<style type="text/css">' +
181 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
183 ' </head><body class="' + cls + '"></body></html>';
187 onRender : function(ct, position)
190 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
191 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
194 this.el.dom.style.border = '0 none';
195 this.el.dom.setAttribute('tabIndex', -1);
196 this.el.addClass('x-hidden hide');
200 if(Roo.isIE){ // fix IE 1px bogus margin
201 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
205 this.frameId = Roo.id();
209 var iframe = this.owner.wrap.createChild({
211 cls: 'form-control', // bootstrap..
215 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
220 this.iframe = iframe.dom;
224 this.doc.designMode = 'on';
227 this.doc.write(this.getDocMarkup());
231 var task = { // must defer to wait for browser to be ready
233 //console.log("run task?" + this.doc.readyState);
235 if(this.doc.body || this.doc.readyState == 'complete'){
237 this.doc.designMode="on";
241 Roo.TaskMgr.stop(task);
242 this.initEditor.defer(10, this);
249 Roo.TaskMgr.start(task);
254 onResize : function(w, h)
256 Roo.log('resize: ' +w + ',' + h );
257 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
261 if(typeof w == 'number'){
263 this.iframe.style.width = w + 'px';
265 if(typeof h == 'number'){
267 this.iframe.style.height = h + 'px';
269 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
276 * Toggles the editor between standard and source edit mode.
277 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
279 toggleSourceEdit : function(sourceEditMode){
281 this.sourceEditMode = sourceEditMode === true;
283 if(this.sourceEditMode){
285 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
288 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
289 //this.iframe.className = '';
292 //this.setSize(this.owner.wrap.getSize());
293 //this.fireEvent('editmodechange', this, this.sourceEditMode);
300 * Protected method that will not generally be called directly. If you need/want
301 * custom HTML cleanup, this is the method you should override.
302 * @param {String} html The HTML to be cleaned
303 * return {String} The cleaned HTML
305 cleanHtml : function(html){
308 if(Roo.isSafari){ // strip safari nonsense
309 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
312 if(html == ' '){
319 * HTML Editor -> Textarea
320 * Protected method that will not generally be called directly. Syncs the contents
321 * of the editor iframe with the textarea.
323 syncValue : function(){
324 if(this.initialized){
325 var bd = (this.doc.body || this.doc.documentElement);
326 //this.cleanUpPaste(); -- this is done else where and causes havoc..
327 var html = bd.innerHTML;
329 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
330 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
332 html = '<div style="'+m[0]+'">' + html + '</div>';
335 html = this.cleanHtml(html);
336 // fix up the special chars.. normaly like back quotes in word...
337 // however we do not want to do this with chinese..
338 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
340 var cc = match.charCodeAt();
342 // Get the character value, handling surrogate pairs
343 if (match.length == 2) {
344 // It's a surrogate pair, calculate the Unicode code point
345 var high = match.charCodeAt(0) - 0xD800;
346 var low = match.charCodeAt(1) - 0xDC00;
347 cc = (high * 0x400) + low + 0x10000;
349 (cc >= 0x4E00 && cc < 0xA000 ) ||
350 (cc >= 0x3400 && cc < 0x4E00 ) ||
351 (cc >= 0xf900 && cc < 0xfb00 )
356 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
357 return "&#" + cc + ";";
364 if(this.owner.fireEvent('beforesync', this, html) !== false){
365 this.el.dom.value = html;
366 this.owner.fireEvent('sync', this, html);
372 * Protected method that will not generally be called directly. Pushes the value of the textarea
373 * into the iframe editor.
375 pushValue : function(){
376 if(this.initialized){
377 var v = this.el.dom.value.trim();
383 if(this.owner.fireEvent('beforepush', this, v) !== false){
384 var d = (this.doc.body || this.doc.documentElement);
387 this.el.dom.value = d.innerHTML;
388 this.owner.fireEvent('push', this, v);
394 deferFocus : function(){
395 this.focus.defer(10, this);
400 if(this.win && !this.sourceEditMode){
407 assignDocWin: function()
409 var iframe = this.iframe;
412 this.doc = iframe.contentWindow.document;
413 this.win = iframe.contentWindow;
415 // if (!Roo.get(this.frameId)) {
418 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
419 // this.win = Roo.get(this.frameId).dom.contentWindow;
421 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
425 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
426 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
431 initEditor : function(){
432 //console.log("INIT EDITOR");
437 this.doc.designMode="on";
439 this.doc.write(this.getDocMarkup());
442 var dbody = (this.doc.body || this.doc.documentElement);
443 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
444 // this copies styles from the containing element into thsi one..
445 // not sure why we need all of this..
446 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
448 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
449 //ss['background-attachment'] = 'fixed'; // w3c
450 dbody.bgProperties = 'fixed'; // ie
451 //Roo.DomHelper.applyStyles(dbody, ss);
452 Roo.EventManager.on(this.doc, {
453 //'mousedown': this.onEditorEvent,
454 'mouseup': this.onEditorEvent,
455 'dblclick': this.onEditorEvent,
456 'click': this.onEditorEvent,
457 'keyup': this.onEditorEvent,
462 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
464 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
465 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
467 this.initialized = true;
469 this.owner.fireEvent('initialize', this);
474 onDestroy : function(){
480 //for (var i =0; i < this.toolbars.length;i++) {
481 // // fixme - ask toolbars for heights?
482 // this.toolbars[i].onDestroy();
485 //this.wrap.dom.innerHTML = '';
486 //this.wrap.remove();
491 onFirstFocus : function(){
496 this.activated = true;
499 if(Roo.isGecko){ // prevent silly gecko errors
501 var s = this.win.getSelection();
502 if(!s.focusNode || s.focusNode.nodeType != 3){
503 var r = s.getRangeAt(0);
504 r.selectNodeContents((this.doc.body || this.doc.documentElement));
509 this.execCmd('useCSS', true);
510 this.execCmd('styleWithCSS', false);
513 this.owner.fireEvent('activate', this);
517 adjustFont: function(btn){
518 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
519 //if(Roo.isSafari){ // safari
522 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
523 if(Roo.isSafari){ // safari
524 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
525 v = (v < 10) ? 10 : v;
526 v = (v > 48) ? 48 : v;
527 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
532 v = Math.max(1, v+adjust);
534 this.execCmd('FontSize', v );
537 onEditorEvent : function(e)
539 this.owner.fireEvent('editorevent', this, e);
540 // this.updateToolbar();
541 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
544 insertTag : function(tg)
546 // could be a bit smarter... -> wrap the current selected tRoo..
547 if (tg.toLowerCase() == 'span' || tg.toLowerCase() == 'code') {
549 range = this.createRange(this.getSelection());
550 var wrappingNode = this.doc.createElement(tg.toLowerCase());
551 wrappingNode.appendChild(range.extractContents());
552 range.insertNode(wrappingNode);
559 this.execCmd("formatblock", tg);
563 insertText : function(txt)
567 var range = this.createRange();
568 range.deleteContents();
569 //alert(Sender.getAttribute('label'));
571 range.insertNode(this.doc.createTextNode(txt));
577 * Executes a Midas editor command on the editor document and performs necessary focus and
578 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
579 * @param {String} cmd The Midas command
580 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
582 relayCmd : function(cmd, value){
584 this.execCmd(cmd, value);
585 this.owner.fireEvent('editorevent', this);
586 //this.updateToolbar();
587 this.owner.deferFocus();
591 * Executes a Midas editor command directly on the editor document.
592 * For visual commands, you should use {@link #relayCmd} instead.
593 * <b>This should only be called after the editor is initialized.</b>
594 * @param {String} cmd The Midas command
595 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
597 execCmd : function(cmd, value){
598 this.doc.execCommand(cmd, false, value === undefined ? null : value);
605 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
607 * @param {String} text | dom node..
609 insertAtCursor : function(text)
618 var r = this.doc.selection.createRange();
629 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
633 // from jquery ui (MIT licenced)
637 if (win.getSelection && win.getSelection().getRangeAt) {
638 range = win.getSelection().getRangeAt(0);
639 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
640 range.insertNode(node);
641 } else if (win.document.selection && win.document.selection.createRange) {
642 // no firefox support
643 var txt = typeof(text) == 'string' ? text : text.outerHTML;
644 win.document.selection.createRange().pasteHTML(txt);
646 // no firefox support
647 var txt = typeof(text) == 'string' ? text : text.outerHTML;
648 this.execCmd('InsertHTML', txt);
657 mozKeyPress : function(e){
659 var c = e.getCharCode(), cmd;
662 c = String.fromCharCode(c).toLowerCase();
676 this.cleanUpPaste.defer(100, this);
692 fixKeys : function(){ // load time branching for fastest keydown performance
695 var k = e.getKey(), r;
698 r = this.doc.selection.createRange();
701 r.pasteHTML('    ');
708 r = this.doc.selection.createRange();
710 var target = r.parentElement();
711 if(!target || target.tagName.toLowerCase() != 'li'){
713 r.pasteHTML('<br />');
719 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
720 this.cleanUpPaste.defer(100, this);
726 }else if(Roo.isOpera){
732 this.execCmd('InsertHTML','    ');
735 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
736 this.cleanUpPaste.defer(100, this);
741 }else if(Roo.isSafari){
747 this.execCmd('InsertText','\t');
751 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
752 this.cleanUpPaste.defer(100, this);
760 getAllAncestors: function()
762 var p = this.getSelectedNode();
765 a.push(p); // push blank onto stack..
766 p = this.getParentElement();
770 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
774 a.push(this.doc.body);
781 getSelection : function()
784 return Roo.isIE ? this.doc.selection : this.win.getSelection();
787 getSelectedNode: function()
789 // this may only work on Gecko!!!
791 // should we cache this!!!!
796 var range = this.createRange(this.getSelection()).cloneRange();
799 var parent = range.parentElement();
801 var testRange = range.duplicate();
802 testRange.moveToElementText(parent);
803 if (testRange.inRange(range)) {
806 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
809 parent = parent.parentElement;
814 // is ancestor a text element.
815 var ac = range.commonAncestorContainer;
816 if (ac.nodeType == 3) {
820 var ar = ac.childNodes;
823 var other_nodes = [];
824 var has_other_nodes = false;
825 for (var i=0;i<ar.length;i++) {
826 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
829 // fullly contained node.
831 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
836 // probably selected..
837 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
838 other_nodes.push(ar[i]);
842 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
847 has_other_nodes = true;
849 if (!nodes.length && other_nodes.length) {
852 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
858 createRange: function(sel)
860 // this has strange effects when using with
861 // top toolbar - not sure if it's a great idea.
862 //this.editor.contentWindow.focus();
863 if (typeof sel != "undefined") {
865 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
867 return this.doc.createRange();
870 return this.doc.createRange();
873 getParentElement: function()
877 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
879 var range = this.createRange(sel);
882 var p = range.commonAncestorContainer;
883 while (p.nodeType == 3) { // text node
894 * Range intersection.. the hard stuff...
898 * [ -- selected range --- ]
902 * if end is before start or hits it. fail.
903 * if start is after end or hits it fail.
905 * if either hits (but other is outside. - then it's not
911 // @see http://www.thismuchiknow.co.uk/?p=64.
912 rangeIntersectsNode : function(range, node)
914 var nodeRange = node.ownerDocument.createRange();
916 nodeRange.selectNode(node);
918 nodeRange.selectNodeContents(node);
921 var rangeStartRange = range.cloneRange();
922 rangeStartRange.collapse(true);
924 var rangeEndRange = range.cloneRange();
925 rangeEndRange.collapse(false);
927 var nodeStartRange = nodeRange.cloneRange();
928 nodeStartRange.collapse(true);
930 var nodeEndRange = nodeRange.cloneRange();
931 nodeEndRange.collapse(false);
933 return rangeStartRange.compareBoundaryPoints(
934 Range.START_TO_START, nodeEndRange) == -1 &&
935 rangeEndRange.compareBoundaryPoints(
936 Range.START_TO_START, nodeStartRange) == 1;
940 rangeCompareNode : function(range, node)
942 var nodeRange = node.ownerDocument.createRange();
944 nodeRange.selectNode(node);
946 nodeRange.selectNodeContents(node);
950 range.collapse(true);
952 nodeRange.collapse(true);
954 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
955 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
957 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
959 var nodeIsBefore = ss == 1;
960 var nodeIsAfter = ee == -1;
962 if (nodeIsBefore && nodeIsAfter) {
965 if (!nodeIsBefore && nodeIsAfter) {
966 return 1; //right trailed.
969 if (nodeIsBefore && !nodeIsAfter) {
970 return 2; // left trailed.
976 // private? - in a new class?
977 cleanUpPaste : function()
979 // cleans up the whole document..
980 Roo.log('cleanuppaste');
982 this.cleanUpChildren(this.doc.body);
983 var clean = this.cleanWordChars(this.doc.body.innerHTML);
984 if (clean != this.doc.body.innerHTML) {
985 this.doc.body.innerHTML = clean;
990 cleanWordChars : function(input) {// change the chars to hex code
991 var he = Roo.HtmlEditorCore;
994 Roo.each(he.swapCodes, function(sw) {
995 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
997 output = output.replace(swapper, sw[1]);
1004 cleanUpChildren : function (n)
1006 if (!n.childNodes.length) {
1009 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1010 this.cleanUpChild(n.childNodes[i]);
1017 cleanUpChild : function (node)
1020 //console.log(node);
1021 if (node.nodeName == "#text") {
1022 // clean up silly Windows -- stuff?
1025 if (node.nodeName == "#comment") {
1026 node.parentNode.removeChild(node);
1027 // clean up silly Windows -- stuff?
1030 var lcname = node.tagName.toLowerCase();
1031 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1032 // whitelist of tags..
1034 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1036 node.parentNode.removeChild(node);
1041 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1043 // spans with no attributes - just remove them..
1044 if ((!node.attributes || !node.attributes.length) && lcname == 'span') {
1045 remove_keep_children = true;
1048 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1049 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1051 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1052 // remove_keep_children = true;
1055 if (remove_keep_children) {
1056 this.cleanUpChildren(node);
1057 // inserts everything just before this node...
1058 while (node.childNodes.length) {
1059 var cn = node.childNodes[0];
1060 node.removeChild(cn);
1061 node.parentNode.insertBefore(cn, node);
1063 node.parentNode.removeChild(node);
1067 if (!node.attributes || !node.attributes.length) {
1072 this.cleanUpChildren(node);
1076 function cleanAttr(n,v)
1079 if (v.match(/^\./) || v.match(/^\//)) {
1082 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1085 if (v.match(/^#/)) {
1088 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1089 node.removeAttribute(n);
1093 var cwhite = this.cwhite;
1094 var cblack = this.cblack;
1096 function cleanStyle(n,v)
1098 if (v.match(/expression/)) { //XSS?? should we even bother..
1099 node.removeAttribute(n);
1103 var parts = v.split(/;/);
1106 Roo.each(parts, function(p) {
1107 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1111 var l = p.split(':').shift().replace(/\s+/g,'');
1112 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1114 if ( cwhite.length && cblack.indexOf(l) > -1) {
1115 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1116 //node.removeAttribute(n);
1120 // only allow 'c whitelisted system attributes'
1121 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1122 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1123 //node.removeAttribute(n);
1134 node.setAttribute(n, clean.join(';'));
1136 node.removeAttribute(n);
1142 for (var i = node.attributes.length-1; i > -1 ; i--) {
1143 var a = node.attributes[i];
1146 if (a.name.toLowerCase().substr(0,2)=='on') {
1147 node.removeAttribute(a.name);
1150 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1151 node.removeAttribute(a.name);
1154 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1155 cleanAttr(a.name,a.value); // fixme..
1158 if (a.name == 'style') {
1159 cleanStyle(a.name,a.value);
1162 /// clean up MS crap..
1163 // tecnically this should be a list of valid class'es..
1166 if (a.name == 'class') {
1167 if (a.value.match(/^Mso/)) {
1168 node.removeAttribute('class');
1171 if (a.value.match(/^body$/)) {
1172 node.removeAttribute('class');
1183 this.cleanUpChildren(node);
1189 * Clean up MS wordisms...
1191 cleanWord : function(node)
1194 this.cleanWord(this.doc.body);
1199 node.nodeName == 'SPAN' &&
1200 !node.hasAttributes() &&
1201 node.childNodes.length == 1 &&
1202 node.firstChild.nodeName == "#text"
1204 var textNode = node.firstChild;
1205 node.removeChild(textNode);
1206 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1207 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1209 node.parentNode.insertBefore(textNode, node);
1210 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1211 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1213 node.parentNode.removeChild(node);
1216 if (node.nodeName == "#text") {
1217 // clean up silly Windows -- stuff?
1220 if (node.nodeName == "#comment") {
1221 node.parentNode.removeChild(node);
1222 // clean up silly Windows -- stuff?
1226 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1227 node.parentNode.removeChild(node);
1230 //Roo.log(node.tagName);
1231 // remove - but keep children..
1232 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|v:|font)/)) {
1233 //Roo.log('-- removed');
1234 while (node.childNodes.length) {
1235 var cn = node.childNodes[0];
1236 node.removeChild(cn);
1237 node.parentNode.insertBefore(cn, node);
1238 // move node to parent - and clean it..
1241 node.parentNode.removeChild(node);
1242 /// no need to iterate chidlren = it's got none..
1243 //this.iterateChildren(node, this.cleanWord);
1247 if (node.className.length) {
1249 var cn = node.className.split(/\W+/);
1251 Roo.each(cn, function(cls) {
1252 if (cls.match(/Mso[a-zA-Z]+/)) {
1257 node.className = cna.length ? cna.join(' ') : '';
1259 node.removeAttribute("class");
1263 if (node.hasAttribute("lang")) {
1264 node.removeAttribute("lang");
1267 if (node.hasAttribute("style")) {
1269 var styles = node.getAttribute("style").split(";");
1271 Roo.each(styles, function(s) {
1272 if (!s.match(/:/)) {
1275 var kv = s.split(":");
1276 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1279 // what ever is left... we allow.
1282 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1283 if (!nstyle.length) {
1284 node.removeAttribute('style');
1287 this.iterateChildren(node, this.cleanWord);
1293 * iterateChildren of a Node, calling fn each time, using this as the scole..
1294 * @param {DomNode} node node to iterate children of.
1295 * @param {Function} fn method of this class to call on each item.
1297 iterateChildren : function(node, fn)
1299 if (!node.childNodes.length) {
1302 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1303 fn.call(this, node.childNodes[i])
1311 * Quite often pasting from word etc.. results in tables with column and widths.
1312 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1315 cleanTableWidths : function(node)
1320 this.cleanTableWidths(this.doc.body);
1325 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1328 Roo.log(node.tagName);
1329 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1330 this.iterateChildren(node, this.cleanTableWidths);
1333 if (node.hasAttribute('width')) {
1334 node.removeAttribute('width');
1338 if (node.hasAttribute("style")) {
1341 var styles = node.getAttribute("style").split(";");
1343 Roo.each(styles, function(s) {
1344 if (!s.match(/:/)) {
1347 var kv = s.split(":");
1348 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1351 // what ever is left... we allow.
1354 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1355 if (!nstyle.length) {
1356 node.removeAttribute('style');
1360 this.iterateChildren(node, this.cleanTableWidths);
1368 domToHTML : function(currentElement, depth, nopadtext) {
1371 nopadtext = nopadtext || false;
1373 if (!currentElement) {
1374 return this.domToHTML(this.doc.body);
1377 //Roo.log(currentElement);
1379 var allText = false;
1380 var nodeName = currentElement.nodeName;
1381 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1383 if (nodeName == '#text') {
1385 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1390 if (nodeName != 'BODY') {
1393 // Prints the node tagName, such as <A>, <IMG>, etc
1396 for(i = 0; i < currentElement.attributes.length;i++) {
1398 var aname = currentElement.attributes.item(i).name;
1399 if (!currentElement.attributes.item(i).value.length) {
1402 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1405 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1414 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1417 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1422 // Traverse the tree
1424 var currentElementChild = currentElement.childNodes.item(i);
1428 while (currentElementChild) {
1429 // Formatting code (indent the tree so it looks nice on the screen)
1430 var nopad = nopadtext;
1431 if (lastnode == 'SPAN') {
1435 if (currentElementChild.nodeName == '#text') {
1436 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1437 toadd = nopadtext ? toadd : toadd.trim();
1438 if (!nopad && toadd.length > 80) {
1439 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1444 currentElementChild = currentElement.childNodes.item(i);
1450 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1452 // Recursively traverse the tree structure of the child node
1453 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1454 lastnode = currentElementChild.nodeName;
1456 currentElementChild=currentElement.childNodes.item(i);
1462 // The remaining code is mostly for formatting the tree
1463 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1468 ret+= "</"+tagName+">";
1474 applyBlacklists : function()
1476 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1477 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1481 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1482 if (b.indexOf(tag) > -1) {
1485 this.white.push(tag);
1489 Roo.each(w, function(tag) {
1490 if (b.indexOf(tag) > -1) {
1493 if (this.white.indexOf(tag) > -1) {
1496 this.white.push(tag);
1501 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1502 if (w.indexOf(tag) > -1) {
1505 this.black.push(tag);
1509 Roo.each(b, function(tag) {
1510 if (w.indexOf(tag) > -1) {
1513 if (this.black.indexOf(tag) > -1) {
1516 this.black.push(tag);
1521 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1522 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1526 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1527 if (b.indexOf(tag) > -1) {
1530 this.cwhite.push(tag);
1534 Roo.each(w, function(tag) {
1535 if (b.indexOf(tag) > -1) {
1538 if (this.cwhite.indexOf(tag) > -1) {
1541 this.cwhite.push(tag);
1546 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1547 if (w.indexOf(tag) > -1) {
1550 this.cblack.push(tag);
1554 Roo.each(b, function(tag) {
1555 if (w.indexOf(tag) > -1) {
1558 if (this.cblack.indexOf(tag) > -1) {
1561 this.cblack.push(tag);
1566 setStylesheets : function(stylesheets)
1568 if(typeof(stylesheets) == 'string'){
1569 Roo.get(this.iframe.contentDocument.head).createChild({
1580 Roo.each(stylesheets, function(s) {
1585 Roo.get(_this.iframe.contentDocument.head).createChild({
1596 removeStylesheets : function()
1600 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1605 setStyle : function(style)
1607 Roo.get(this.iframe.contentDocument.head).createChild({
1616 // hide stuff that is not compatible
1634 * @cfg {String} fieldClass @hide
1637 * @cfg {String} focusClass @hide
1640 * @cfg {String} autoCreate @hide
1643 * @cfg {String} inputType @hide
1646 * @cfg {String} invalidClass @hide
1649 * @cfg {String} invalidText @hide
1652 * @cfg {String} msgFx @hide
1655 * @cfg {String} validateOnBlur @hide
1659 Roo.HtmlEditorCore.white = [
1660 'area', 'br', 'img', 'input', 'hr', 'wbr',
1662 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1663 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1664 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1665 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1666 'table', 'ul', 'xmp',
1668 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1671 'dir', 'menu', 'ol', 'ul', 'dl',
1677 Roo.HtmlEditorCore.black = [
1678 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1680 'base', 'basefont', 'bgsound', 'blink', 'body',
1681 'frame', 'frameset', 'head', 'html', 'ilayer',
1682 'iframe', 'layer', 'link', 'meta', 'object',
1683 'script', 'style' ,'title', 'xml' // clean later..
1685 Roo.HtmlEditorCore.clean = [
1686 'script', 'style', 'title', 'xml'
1688 Roo.HtmlEditorCore.remove = [
1693 Roo.HtmlEditorCore.ablack = [
1697 Roo.HtmlEditorCore.aclean = [
1698 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1702 Roo.HtmlEditorCore.pwhite= [
1703 'http', 'https', 'mailto'
1706 // white listed style attributes.
1707 Roo.HtmlEditorCore.cwhite= [
1708 // 'text-align', /// default is to allow most things..
1714 // black listed style attributes.
1715 Roo.HtmlEditorCore.cblack= [
1716 // 'font-size' -- this can be set by the project
1720 Roo.HtmlEditorCore.swapCodes =[