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;
1044 if (!node.attributes || !node.attributes.length) {
1046 if (lcname == 'span' &&
1047 node.childNodes.length == 1 &&
1048 node.childNodes[0].nodeName.toLowerCase() == 'span'
1050 remove_keep_children = true;
1054 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1055 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1057 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1058 // remove_keep_children = true;
1061 if (remove_keep_children) {
1062 this.cleanUpChildren(node);
1063 // inserts everything just before this node...
1064 while (node.childNodes.length) {
1065 var cn = node.childNodes[0];
1066 node.removeChild(cn);
1067 node.parentNode.insertBefore(cn, node);
1069 node.parentNode.removeChild(node);
1073 if (!node.attributes || !node.attributes.length) {
1078 this.cleanUpChildren(node);
1082 function cleanAttr(n,v)
1085 if (v.match(/^\./) || v.match(/^\//)) {
1088 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1091 if (v.match(/^#/)) {
1094 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1095 node.removeAttribute(n);
1099 var cwhite = this.cwhite;
1100 var cblack = this.cblack;
1102 function cleanStyle(n,v)
1104 if (v.match(/expression/)) { //XSS?? should we even bother..
1105 node.removeAttribute(n);
1109 var parts = v.split(/;/);
1112 Roo.each(parts, function(p) {
1113 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1117 var l = p.split(':').shift().replace(/\s+/g,'');
1118 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1120 if ( cwhite.length && cblack.indexOf(l) > -1) {
1121 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1122 //node.removeAttribute(n);
1126 // only allow 'c whitelisted system attributes'
1127 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1128 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1129 //node.removeAttribute(n);
1140 node.setAttribute(n, clean.join(';'));
1142 node.removeAttribute(n);
1148 for (var i = node.attributes.length-1; i > -1 ; i--) {
1149 var a = node.attributes[i];
1152 if (a.name.toLowerCase().substr(0,2)=='on') {
1153 node.removeAttribute(a.name);
1156 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1157 node.removeAttribute(a.name);
1160 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1161 cleanAttr(a.name,a.value); // fixme..
1164 if (a.name == 'style') {
1165 cleanStyle(a.name,a.value);
1168 /// clean up MS crap..
1169 // tecnically this should be a list of valid class'es..
1172 if (a.name == 'class') {
1173 if (a.value.match(/^Mso/)) {
1174 node.className = '';
1177 if (a.value.match(/^body$/)) {
1178 node.className = '';
1189 this.cleanUpChildren(node);
1195 * Clean up MS wordisms...
1197 cleanWord : function(node)
1200 this.cleanWord(this.doc.body);
1205 node.nodeName == 'SPAN' &&
1206 !node.hasAttributes() &&
1207 node.childNodes.length == 1 &&
1208 node.firstChild.nodeName == "#text"
1210 var textNode = node.firstChild;
1211 node.removeChild(textNode);
1212 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1213 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1215 node.parentNode.insertBefore(textNode, node);
1216 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1217 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1219 node.parentNode.removeChild(node);
1222 if (node.nodeName == "#text") {
1223 // clean up silly Windows -- stuff?
1226 if (node.nodeName == "#comment") {
1227 node.parentNode.removeChild(node);
1228 // clean up silly Windows -- stuff?
1232 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1233 node.parentNode.removeChild(node);
1237 // remove - but keep children..
1238 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1239 while (node.childNodes.length) {
1240 var cn = node.childNodes[0];
1241 node.removeChild(cn);
1242 node.parentNode.insertBefore(cn, node);
1244 node.parentNode.removeChild(node);
1245 this.iterateChildren(node, this.cleanWord);
1249 if (node.className.length) {
1251 var cn = node.className.split(/\W+/);
1253 Roo.each(cn, function(cls) {
1254 if (cls.match(/Mso[a-zA-Z]+/)) {
1259 node.className = cna.length ? cna.join(' ') : '';
1261 node.removeAttribute("class");
1265 if (node.hasAttribute("lang")) {
1266 node.removeAttribute("lang");
1269 if (node.hasAttribute("style")) {
1271 var styles = node.getAttribute("style").split(";");
1273 Roo.each(styles, function(s) {
1274 if (!s.match(/:/)) {
1277 var kv = s.split(":");
1278 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1281 // what ever is left... we allow.
1284 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1285 if (!nstyle.length) {
1286 node.removeAttribute('style');
1289 this.iterateChildren(node, this.cleanWord);
1295 * iterateChildren of a Node, calling fn each time, using this as the scole..
1296 * @param {DomNode} node node to iterate children of.
1297 * @param {Function} fn method of this class to call on each item.
1299 iterateChildren : function(node, fn)
1301 if (!node.childNodes.length) {
1304 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1305 fn.call(this, node.childNodes[i])
1313 * Quite often pasting from word etc.. results in tables with column and widths.
1314 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1317 cleanTableWidths : function(node)
1322 this.cleanTableWidths(this.doc.body);
1327 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1330 Roo.log(node.tagName);
1331 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1332 this.iterateChildren(node, this.cleanTableWidths);
1335 if (node.hasAttribute('width')) {
1336 node.removeAttribute('width');
1340 if (node.hasAttribute("style")) {
1343 var styles = node.getAttribute("style").split(";");
1345 Roo.each(styles, function(s) {
1346 if (!s.match(/:/)) {
1349 var kv = s.split(":");
1350 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1353 // what ever is left... we allow.
1356 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1357 if (!nstyle.length) {
1358 node.removeAttribute('style');
1362 this.iterateChildren(node, this.cleanTableWidths);
1370 domToHTML : function(currentElement, depth, nopadtext) {
1373 nopadtext = nopadtext || false;
1375 if (!currentElement) {
1376 return this.domToHTML(this.doc.body);
1379 //Roo.log(currentElement);
1381 var allText = false;
1382 var nodeName = currentElement.nodeName;
1383 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1385 if (nodeName == '#text') {
1387 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1392 if (nodeName != 'BODY') {
1395 // Prints the node tagName, such as <A>, <IMG>, etc
1398 for(i = 0; i < currentElement.attributes.length;i++) {
1400 var aname = currentElement.attributes.item(i).name;
1401 if (!currentElement.attributes.item(i).value.length) {
1404 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1407 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1416 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1419 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1424 // Traverse the tree
1426 var currentElementChild = currentElement.childNodes.item(i);
1430 while (currentElementChild) {
1431 // Formatting code (indent the tree so it looks nice on the screen)
1432 var nopad = nopadtext;
1433 if (lastnode == 'SPAN') {
1437 if (currentElementChild.nodeName == '#text') {
1438 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1439 toadd = nopadtext ? toadd : toadd.trim();
1440 if (!nopad && toadd.length > 80) {
1441 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1446 currentElementChild = currentElement.childNodes.item(i);
1452 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1454 // Recursively traverse the tree structure of the child node
1455 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1456 lastnode = currentElementChild.nodeName;
1458 currentElementChild=currentElement.childNodes.item(i);
1464 // The remaining code is mostly for formatting the tree
1465 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1470 ret+= "</"+tagName+">";
1476 applyBlacklists : function()
1478 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1479 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1483 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1484 if (b.indexOf(tag) > -1) {
1487 this.white.push(tag);
1491 Roo.each(w, function(tag) {
1492 if (b.indexOf(tag) > -1) {
1495 if (this.white.indexOf(tag) > -1) {
1498 this.white.push(tag);
1503 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1504 if (w.indexOf(tag) > -1) {
1507 this.black.push(tag);
1511 Roo.each(b, function(tag) {
1512 if (w.indexOf(tag) > -1) {
1515 if (this.black.indexOf(tag) > -1) {
1518 this.black.push(tag);
1523 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1524 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1528 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1529 if (b.indexOf(tag) > -1) {
1532 this.cwhite.push(tag);
1536 Roo.each(w, function(tag) {
1537 if (b.indexOf(tag) > -1) {
1540 if (this.cwhite.indexOf(tag) > -1) {
1543 this.cwhite.push(tag);
1548 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1549 if (w.indexOf(tag) > -1) {
1552 this.cblack.push(tag);
1556 Roo.each(b, function(tag) {
1557 if (w.indexOf(tag) > -1) {
1560 if (this.cblack.indexOf(tag) > -1) {
1563 this.cblack.push(tag);
1568 setStylesheets : function(stylesheets)
1570 if(typeof(stylesheets) == 'string'){
1571 Roo.get(this.iframe.contentDocument.head).createChild({
1582 Roo.each(stylesheets, function(s) {
1587 Roo.get(_this.iframe.contentDocument.head).createChild({
1598 removeStylesheets : function()
1602 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1607 setStyle : function(style)
1609 Roo.get(this.iframe.contentDocument.head).createChild({
1618 // hide stuff that is not compatible
1636 * @cfg {String} fieldClass @hide
1639 * @cfg {String} focusClass @hide
1642 * @cfg {String} autoCreate @hide
1645 * @cfg {String} inputType @hide
1648 * @cfg {String} invalidClass @hide
1651 * @cfg {String} invalidText @hide
1654 * @cfg {String} msgFx @hide
1657 * @cfg {String} validateOnBlur @hide
1661 Roo.HtmlEditorCore.white = [
1662 'area', 'br', 'img', 'input', 'hr', 'wbr',
1664 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1665 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1666 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1667 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1668 'table', 'ul', 'xmp',
1670 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1673 'dir', 'menu', 'ol', 'ul', 'dl',
1679 Roo.HtmlEditorCore.black = [
1680 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1682 'base', 'basefont', 'bgsound', 'blink', 'body',
1683 'frame', 'frameset', 'head', 'html', 'ilayer',
1684 'iframe', 'layer', 'link', 'meta', 'object',
1685 'script', 'style' ,'title', 'xml' // clean later..
1687 Roo.HtmlEditorCore.clean = [
1688 'script', 'style', 'title', 'xml'
1690 Roo.HtmlEditorCore.remove = [
1695 Roo.HtmlEditorCore.ablack = [
1699 Roo.HtmlEditorCore.aclean = [
1700 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1704 Roo.HtmlEditorCore.pwhite= [
1705 'http', 'https', 'mailto'
1708 // white listed style attributes.
1709 Roo.HtmlEditorCore.cwhite= [
1710 // 'text-align', /// default is to allow most things..
1716 // black listed style attributes.
1717 Roo.HtmlEditorCore.cblack= [
1718 // 'font-size' -- this can be set by the project
1722 Roo.HtmlEditorCore.swapCodes =[