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 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1044 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1046 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1047 // remove_keep_children = true;
1050 if (remove_keep_children) {
1051 this.cleanUpChildren(node);
1052 // inserts everything just before this node...
1053 while (node.childNodes.length) {
1054 var cn = node.childNodes[0];
1055 node.removeChild(cn);
1056 node.parentNode.insertBefore(cn, node);
1058 node.parentNode.removeChild(node);
1062 if (!node.attributes || !node.attributes.length) {
1063 this.cleanUpChildren(node);
1067 function cleanAttr(n,v)
1070 if (v.match(/^\./) || v.match(/^\//)) {
1073 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1076 if (v.match(/^#/)) {
1079 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1080 node.removeAttribute(n);
1084 var cwhite = this.cwhite;
1085 var cblack = this.cblack;
1087 function cleanStyle(n,v)
1089 if (v.match(/expression/)) { //XSS?? should we even bother..
1090 node.removeAttribute(n);
1094 var parts = v.split(/;/);
1097 Roo.each(parts, function(p) {
1098 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1102 var l = p.split(':').shift().replace(/\s+/g,'');
1103 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1105 if ( cwhite.length && cblack.indexOf(l) > -1) {
1106 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1107 //node.removeAttribute(n);
1111 // only allow 'c whitelisted system attributes'
1112 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1113 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1114 //node.removeAttribute(n);
1125 node.setAttribute(n, clean.join(';'));
1127 node.removeAttribute(n);
1133 for (var i = node.attributes.length-1; i > -1 ; i--) {
1134 var a = node.attributes[i];
1137 if (a.name.toLowerCase().substr(0,2)=='on') {
1138 node.removeAttribute(a.name);
1141 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1142 node.removeAttribute(a.name);
1145 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1146 cleanAttr(a.name,a.value); // fixme..
1149 if (a.name == 'style') {
1150 cleanStyle(a.name,a.value);
1153 /// clean up MS crap..
1154 // tecnically this should be a list of valid class'es..
1157 if (a.name == 'class') {
1158 if (a.value.match(/^Mso/)) {
1159 node.className = '';
1162 if (a.value.match(/^body$/)) {
1163 node.className = '';
1174 this.cleanUpChildren(node);
1180 * Clean up MS wordisms...
1182 cleanWord : function(node)
1185 this.cleanWord(this.doc.body);
1190 node.nodeName == 'SPAN' &&
1191 !node.hasAttributes() &&
1192 node.childNodes.length == 1 &&
1193 node.firstChild.nodeName == "#text"
1195 var textNode = node.firstChild;
1196 node.removeChild(textNode);
1197 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1198 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1200 node.parentNode.insertBefore(textNode, node);
1201 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1202 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1204 node.parentNode.removeChild(node);
1207 if (node.nodeName == "#text") {
1208 // clean up silly Windows -- stuff?
1211 if (node.nodeName == "#comment") {
1212 node.parentNode.removeChild(node);
1213 // clean up silly Windows -- stuff?
1217 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1218 node.parentNode.removeChild(node);
1222 // remove - but keep children..
1223 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|font)/)) {
1224 while (node.childNodes.length) {
1225 var cn = node.childNodes[0];
1226 node.removeChild(cn);
1227 node.parentNode.insertBefore(cn, node);
1229 node.parentNode.removeChild(node);
1230 this.iterateChildren(node, this.cleanWord);
1234 if (node.className.length) {
1236 var cn = node.className.split(/\W+/);
1238 Roo.each(cn, function(cls) {
1239 if (cls.match(/Mso[a-zA-Z]+/)) {
1244 node.className = cna.length ? cna.join(' ') : '';
1246 node.removeAttribute("class");
1250 if (node.hasAttribute("lang")) {
1251 node.removeAttribute("lang");
1254 if (node.hasAttribute("style")) {
1256 var styles = node.getAttribute("style").split(";");
1258 Roo.each(styles, function(s) {
1259 if (!s.match(/:/)) {
1262 var kv = s.split(":");
1263 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1266 // what ever is left... we allow.
1269 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1270 if (!nstyle.length) {
1271 node.removeAttribute('style');
1274 this.iterateChildren(node, this.cleanWord);
1280 * iterateChildren of a Node, calling fn each time, using this as the scole..
1281 * @param {DomNode} node node to iterate children of.
1282 * @param {Function} fn method of this class to call on each item.
1284 iterateChildren : function(node, fn)
1286 if (!node.childNodes.length) {
1289 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1290 fn.call(this, node.childNodes[i])
1298 * Quite often pasting from word etc.. results in tables with column and widths.
1299 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1302 cleanTableWidths : function(node)
1307 this.cleanTableWidths(this.doc.body);
1312 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1315 Roo.log(node.tagName);
1316 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1317 this.iterateChildren(node, this.cleanTableWidths);
1320 if (node.hasAttribute('width')) {
1321 node.removeAttribute('width');
1325 if (node.hasAttribute("style")) {
1328 var styles = node.getAttribute("style").split(";");
1330 Roo.each(styles, function(s) {
1331 if (!s.match(/:/)) {
1334 var kv = s.split(":");
1335 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1338 // what ever is left... we allow.
1341 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1342 if (!nstyle.length) {
1343 node.removeAttribute('style');
1347 this.iterateChildren(node, this.cleanTableWidths);
1355 domToHTML : function(currentElement, depth, nopadtext) {
1358 nopadtext = nopadtext || false;
1360 if (!currentElement) {
1361 return this.domToHTML(this.doc.body);
1364 //Roo.log(currentElement);
1366 var allText = false;
1367 var nodeName = currentElement.nodeName;
1368 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1370 if (nodeName == '#text') {
1372 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1377 if (nodeName != 'BODY') {
1380 // Prints the node tagName, such as <A>, <IMG>, etc
1383 for(i = 0; i < currentElement.attributes.length;i++) {
1385 var aname = currentElement.attributes.item(i).name;
1386 if (!currentElement.attributes.item(i).value.length) {
1389 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1392 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1401 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1404 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1409 // Traverse the tree
1411 var currentElementChild = currentElement.childNodes.item(i);
1415 while (currentElementChild) {
1416 // Formatting code (indent the tree so it looks nice on the screen)
1417 var nopad = nopadtext;
1418 if (lastnode == 'SPAN') {
1422 if (currentElementChild.nodeName == '#text') {
1423 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1424 toadd = nopadtext ? toadd : toadd.trim();
1425 if (!nopad && toadd.length > 80) {
1426 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1431 currentElementChild = currentElement.childNodes.item(i);
1437 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1439 // Recursively traverse the tree structure of the child node
1440 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1441 lastnode = currentElementChild.nodeName;
1443 currentElementChild=currentElement.childNodes.item(i);
1449 // The remaining code is mostly for formatting the tree
1450 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1455 ret+= "</"+tagName+">";
1461 applyBlacklists : function()
1463 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1464 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1468 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1469 if (b.indexOf(tag) > -1) {
1472 this.white.push(tag);
1476 Roo.each(w, function(tag) {
1477 if (b.indexOf(tag) > -1) {
1480 if (this.white.indexOf(tag) > -1) {
1483 this.white.push(tag);
1488 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1489 if (w.indexOf(tag) > -1) {
1492 this.black.push(tag);
1496 Roo.each(b, function(tag) {
1497 if (w.indexOf(tag) > -1) {
1500 if (this.black.indexOf(tag) > -1) {
1503 this.black.push(tag);
1508 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1509 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1513 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1514 if (b.indexOf(tag) > -1) {
1517 this.cwhite.push(tag);
1521 Roo.each(w, function(tag) {
1522 if (b.indexOf(tag) > -1) {
1525 if (this.cwhite.indexOf(tag) > -1) {
1528 this.cwhite.push(tag);
1533 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1534 if (w.indexOf(tag) > -1) {
1537 this.cblack.push(tag);
1541 Roo.each(b, function(tag) {
1542 if (w.indexOf(tag) > -1) {
1545 if (this.cblack.indexOf(tag) > -1) {
1548 this.cblack.push(tag);
1553 setStylesheets : function(stylesheets)
1555 if(typeof(stylesheets) == 'string'){
1556 Roo.get(this.iframe.contentDocument.head).createChild({
1567 Roo.each(stylesheets, function(s) {
1572 Roo.get(_this.iframe.contentDocument.head).createChild({
1583 removeStylesheets : function()
1587 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1592 setStyle : function(style)
1594 Roo.get(this.iframe.contentDocument.head).createChild({
1603 // hide stuff that is not compatible
1621 * @cfg {String} fieldClass @hide
1624 * @cfg {String} focusClass @hide
1627 * @cfg {String} autoCreate @hide
1630 * @cfg {String} inputType @hide
1633 * @cfg {String} invalidClass @hide
1636 * @cfg {String} invalidText @hide
1639 * @cfg {String} msgFx @hide
1642 * @cfg {String} validateOnBlur @hide
1646 Roo.HtmlEditorCore.white = [
1647 'area', 'br', 'img', 'input', 'hr', 'wbr',
1649 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1650 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1651 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1652 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1653 'table', 'ul', 'xmp',
1655 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1658 'dir', 'menu', 'ol', 'ul', 'dl',
1664 Roo.HtmlEditorCore.black = [
1665 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1667 'base', 'basefont', 'bgsound', 'blink', 'body',
1668 'frame', 'frameset', 'head', 'html', 'ilayer',
1669 'iframe', 'layer', 'link', 'meta', 'object',
1670 'script', 'style' ,'title', 'xml' // clean later..
1672 Roo.HtmlEditorCore.clean = [
1673 'script', 'style', 'title', 'xml'
1675 Roo.HtmlEditorCore.remove = [
1680 Roo.HtmlEditorCore.ablack = [
1684 Roo.HtmlEditorCore.aclean = [
1685 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1689 Roo.HtmlEditorCore.pwhite= [
1690 'http', 'https', 'mailto'
1693 // white listed style attributes.
1694 Roo.HtmlEditorCore.cwhite= [
1695 // 'text-align', /// default is to allow most things..
1701 // black listed style attributes.
1702 Roo.HtmlEditorCore.cblack= [
1703 // 'font-size' -- this can be set by the project
1707 Roo.HtmlEditorCore.swapCodes =[