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 contenteditable="true" data-enable-grammerly="true" 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' ||
548 tg.toLowerCase() == 'code' ||
549 tg.toLowerCase() == 'sup' ||
550 tg.toLowerCase() == 'sub'
553 range = this.createRange(this.getSelection());
554 var wrappingNode = this.doc.createElement(tg.toLowerCase());
555 wrappingNode.appendChild(range.extractContents());
556 range.insertNode(wrappingNode);
563 this.execCmd("formatblock", tg);
567 insertText : function(txt)
571 var range = this.createRange();
572 range.deleteContents();
573 //alert(Sender.getAttribute('label'));
575 range.insertNode(this.doc.createTextNode(txt));
581 * Executes a Midas editor command on the editor document and performs necessary focus and
582 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
583 * @param {String} cmd The Midas command
584 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
586 relayCmd : function(cmd, value){
588 this.execCmd(cmd, value);
589 this.owner.fireEvent('editorevent', this);
590 //this.updateToolbar();
591 this.owner.deferFocus();
595 * Executes a Midas editor command directly on the editor document.
596 * For visual commands, you should use {@link #relayCmd} instead.
597 * <b>This should only be called after the editor is initialized.</b>
598 * @param {String} cmd The Midas command
599 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
601 execCmd : function(cmd, value){
602 this.doc.execCommand(cmd, false, value === undefined ? null : value);
609 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
611 * @param {String} text | dom node..
613 insertAtCursor : function(text)
622 var r = this.doc.selection.createRange();
633 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
637 // from jquery ui (MIT licenced)
641 if (win.getSelection && win.getSelection().getRangeAt) {
642 range = win.getSelection().getRangeAt(0);
643 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
644 range.insertNode(node);
645 } else if (win.document.selection && win.document.selection.createRange) {
646 // no firefox support
647 var txt = typeof(text) == 'string' ? text : text.outerHTML;
648 win.document.selection.createRange().pasteHTML(txt);
650 // no firefox support
651 var txt = typeof(text) == 'string' ? text : text.outerHTML;
652 this.execCmd('InsertHTML', txt);
661 mozKeyPress : function(e){
663 var c = e.getCharCode(), cmd;
666 c = String.fromCharCode(c).toLowerCase();
680 this.cleanUpPaste.defer(100, this);
696 fixKeys : function(){ // load time branching for fastest keydown performance
699 var k = e.getKey(), r;
702 r = this.doc.selection.createRange();
705 r.pasteHTML('    ');
712 r = this.doc.selection.createRange();
714 var target = r.parentElement();
715 if(!target || target.tagName.toLowerCase() != 'li'){
717 r.pasteHTML('<br />');
723 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
724 this.cleanUpPaste.defer(100, this);
730 }else if(Roo.isOpera){
736 this.execCmd('InsertHTML','    ');
739 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
740 this.cleanUpPaste.defer(100, this);
745 }else if(Roo.isSafari){
751 this.execCmd('InsertText','\t');
755 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
756 this.cleanUpPaste.defer(100, this);
764 getAllAncestors: function()
766 var p = this.getSelectedNode();
769 a.push(p); // push blank onto stack..
770 p = this.getParentElement();
774 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
778 a.push(this.doc.body);
785 getSelection : function()
788 return Roo.isIE ? this.doc.selection : this.win.getSelection();
791 getSelectedNode: function()
793 // this may only work on Gecko!!!
795 // should we cache this!!!!
800 var range = this.createRange(this.getSelection()).cloneRange();
803 var parent = range.parentElement();
805 var testRange = range.duplicate();
806 testRange.moveToElementText(parent);
807 if (testRange.inRange(range)) {
810 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
813 parent = parent.parentElement;
818 // is ancestor a text element.
819 var ac = range.commonAncestorContainer;
820 if (ac.nodeType == 3) {
824 var ar = ac.childNodes;
827 var other_nodes = [];
828 var has_other_nodes = false;
829 for (var i=0;i<ar.length;i++) {
830 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
833 // fullly contained node.
835 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
840 // probably selected..
841 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
842 other_nodes.push(ar[i]);
846 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
851 has_other_nodes = true;
853 if (!nodes.length && other_nodes.length) {
856 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
862 createRange: function(sel)
864 // this has strange effects when using with
865 // top toolbar - not sure if it's a great idea.
866 //this.editor.contentWindow.focus();
867 if (typeof sel != "undefined") {
869 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
871 return this.doc.createRange();
874 return this.doc.createRange();
877 getParentElement: function()
881 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
883 var range = this.createRange(sel);
886 var p = range.commonAncestorContainer;
887 while (p.nodeType == 3) { // text node
898 * Range intersection.. the hard stuff...
902 * [ -- selected range --- ]
906 * if end is before start or hits it. fail.
907 * if start is after end or hits it fail.
909 * if either hits (but other is outside. - then it's not
915 // @see http://www.thismuchiknow.co.uk/?p=64.
916 rangeIntersectsNode : function(range, node)
918 var nodeRange = node.ownerDocument.createRange();
920 nodeRange.selectNode(node);
922 nodeRange.selectNodeContents(node);
925 var rangeStartRange = range.cloneRange();
926 rangeStartRange.collapse(true);
928 var rangeEndRange = range.cloneRange();
929 rangeEndRange.collapse(false);
931 var nodeStartRange = nodeRange.cloneRange();
932 nodeStartRange.collapse(true);
934 var nodeEndRange = nodeRange.cloneRange();
935 nodeEndRange.collapse(false);
937 return rangeStartRange.compareBoundaryPoints(
938 Range.START_TO_START, nodeEndRange) == -1 &&
939 rangeEndRange.compareBoundaryPoints(
940 Range.START_TO_START, nodeStartRange) == 1;
944 rangeCompareNode : function(range, node)
946 var nodeRange = node.ownerDocument.createRange();
948 nodeRange.selectNode(node);
950 nodeRange.selectNodeContents(node);
954 range.collapse(true);
956 nodeRange.collapse(true);
958 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
959 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
961 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
963 var nodeIsBefore = ss == 1;
964 var nodeIsAfter = ee == -1;
966 if (nodeIsBefore && nodeIsAfter) {
969 if (!nodeIsBefore && nodeIsAfter) {
970 return 1; //right trailed.
973 if (nodeIsBefore && !nodeIsAfter) {
974 return 2; // left trailed.
980 // private? - in a new class?
981 cleanUpPaste : function()
983 // cleans up the whole document..
984 Roo.log('cleanuppaste');
986 this.cleanUpChildren(this.doc.body);
987 var clean = this.cleanWordChars(this.doc.body.innerHTML);
988 if (clean != this.doc.body.innerHTML) {
989 this.doc.body.innerHTML = clean;
994 cleanWordChars : function(input) {// change the chars to hex code
995 var he = Roo.HtmlEditorCore;
998 Roo.each(he.swapCodes, function(sw) {
999 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1001 output = output.replace(swapper, sw[1]);
1008 cleanUpChildren : function (n)
1010 if (!n.childNodes.length) {
1013 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1014 this.cleanUpChild(n.childNodes[i]);
1021 cleanUpChild : function (node)
1024 //console.log(node);
1025 if (node.nodeName == "#text") {
1026 // clean up silly Windows -- stuff?
1029 if (node.nodeName == "#comment") {
1030 node.parentNode.removeChild(node);
1031 // clean up silly Windows -- stuff?
1034 var lcname = node.tagName.toLowerCase();
1035 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1036 // whitelist of tags..
1038 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1040 node.parentNode.removeChild(node);
1045 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1047 // spans with no attributes - just remove them..
1048 if ((!node.attributes || !node.attributes.length) && lcname == 'span') {
1049 remove_keep_children = true;
1052 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1053 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1055 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1056 // remove_keep_children = true;
1059 if (remove_keep_children) {
1060 this.cleanUpChildren(node);
1061 // inserts everything just before this node...
1062 while (node.childNodes.length) {
1063 var cn = node.childNodes[0];
1064 node.removeChild(cn);
1065 node.parentNode.insertBefore(cn, node);
1067 node.parentNode.removeChild(node);
1071 if (!node.attributes || !node.attributes.length) {
1076 this.cleanUpChildren(node);
1080 function cleanAttr(n,v)
1083 if (v.match(/^\./) || v.match(/^\//)) {
1086 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1089 if (v.match(/^#/)) {
1092 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1093 node.removeAttribute(n);
1097 var cwhite = this.cwhite;
1098 var cblack = this.cblack;
1100 function cleanStyle(n,v)
1102 if (v.match(/expression/)) { //XSS?? should we even bother..
1103 node.removeAttribute(n);
1107 var parts = v.split(/;/);
1110 Roo.each(parts, function(p) {
1111 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1115 var l = p.split(':').shift().replace(/\s+/g,'');
1116 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1118 if ( cwhite.length && cblack.indexOf(l) > -1) {
1119 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1120 //node.removeAttribute(n);
1124 // only allow 'c whitelisted system attributes'
1125 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1126 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1127 //node.removeAttribute(n);
1138 node.setAttribute(n, clean.join(';'));
1140 node.removeAttribute(n);
1146 for (var i = node.attributes.length-1; i > -1 ; i--) {
1147 var a = node.attributes[i];
1150 if (a.name.toLowerCase().substr(0,2)=='on') {
1151 node.removeAttribute(a.name);
1154 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1155 node.removeAttribute(a.name);
1158 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1159 cleanAttr(a.name,a.value); // fixme..
1162 if (a.name == 'style') {
1163 cleanStyle(a.name,a.value);
1166 /// clean up MS crap..
1167 // tecnically this should be a list of valid class'es..
1170 if (a.name == 'class') {
1171 if (a.value.match(/^Mso/)) {
1172 node.removeAttribute('class');
1175 if (a.value.match(/^body$/)) {
1176 node.removeAttribute('class');
1187 this.cleanUpChildren(node);
1193 * Clean up MS wordisms...
1195 cleanWord : function(node)
1198 this.cleanWord(this.doc.body);
1203 node.nodeName == 'SPAN' &&
1204 !node.hasAttributes() &&
1205 node.childNodes.length == 1 &&
1206 node.firstChild.nodeName == "#text"
1208 var textNode = node.firstChild;
1209 node.removeChild(textNode);
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.insertBefore(textNode, node);
1214 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1215 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1217 node.parentNode.removeChild(node);
1220 if (node.nodeName == "#text") {
1221 // clean up silly Windows -- stuff?
1224 if (node.nodeName == "#comment") {
1225 node.parentNode.removeChild(node);
1226 // clean up silly Windows -- stuff?
1230 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1231 node.parentNode.removeChild(node);
1234 //Roo.log(node.tagName);
1235 // remove - but keep children..
1236 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|v:|font)/)) {
1237 //Roo.log('-- removed');
1238 while (node.childNodes.length) {
1239 var cn = node.childNodes[0];
1240 node.removeChild(cn);
1241 node.parentNode.insertBefore(cn, node);
1242 // move node to parent - and clean it..
1245 node.parentNode.removeChild(node);
1246 /// no need to iterate chidlren = it's got none..
1247 //this.iterateChildren(node, this.cleanWord);
1251 if (node.className.length) {
1253 var cn = node.className.split(/\W+/);
1255 Roo.each(cn, function(cls) {
1256 if (cls.match(/Mso[a-zA-Z]+/)) {
1261 node.className = cna.length ? cna.join(' ') : '';
1263 node.removeAttribute("class");
1267 if (node.hasAttribute("lang")) {
1268 node.removeAttribute("lang");
1271 if (node.hasAttribute("style")) {
1273 var styles = node.getAttribute("style").split(";");
1275 Roo.each(styles, function(s) {
1276 if (!s.match(/:/)) {
1279 var kv = s.split(":");
1280 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1283 // what ever is left... we allow.
1286 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1287 if (!nstyle.length) {
1288 node.removeAttribute('style');
1291 this.iterateChildren(node, this.cleanWord);
1297 * iterateChildren of a Node, calling fn each time, using this as the scole..
1298 * @param {DomNode} node node to iterate children of.
1299 * @param {Function} fn method of this class to call on each item.
1301 iterateChildren : function(node, fn)
1303 if (!node.childNodes.length) {
1306 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1307 fn.call(this, node.childNodes[i])
1315 * Quite often pasting from word etc.. results in tables with column and widths.
1316 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1319 cleanTableWidths : function(node)
1324 this.cleanTableWidths(this.doc.body);
1329 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1332 Roo.log(node.tagName);
1333 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1334 this.iterateChildren(node, this.cleanTableWidths);
1337 if (node.hasAttribute('width')) {
1338 node.removeAttribute('width');
1342 if (node.hasAttribute("style")) {
1345 var styles = node.getAttribute("style").split(";");
1347 Roo.each(styles, function(s) {
1348 if (!s.match(/:/)) {
1351 var kv = s.split(":");
1352 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1355 // what ever is left... we allow.
1358 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1359 if (!nstyle.length) {
1360 node.removeAttribute('style');
1364 this.iterateChildren(node, this.cleanTableWidths);
1372 domToHTML : function(currentElement, depth, nopadtext) {
1375 nopadtext = nopadtext || false;
1377 if (!currentElement) {
1378 return this.domToHTML(this.doc.body);
1381 //Roo.log(currentElement);
1383 var allText = false;
1384 var nodeName = currentElement.nodeName;
1385 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1387 if (nodeName == '#text') {
1389 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1394 if (nodeName != 'BODY') {
1397 // Prints the node tagName, such as <A>, <IMG>, etc
1400 for(i = 0; i < currentElement.attributes.length;i++) {
1402 var aname = currentElement.attributes.item(i).name;
1403 if (!currentElement.attributes.item(i).value.length) {
1406 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1409 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1418 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1421 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1426 // Traverse the tree
1428 var currentElementChild = currentElement.childNodes.item(i);
1432 while (currentElementChild) {
1433 // Formatting code (indent the tree so it looks nice on the screen)
1434 var nopad = nopadtext;
1435 if (lastnode == 'SPAN') {
1439 if (currentElementChild.nodeName == '#text') {
1440 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1441 toadd = nopadtext ? toadd : toadd.trim();
1442 if (!nopad && toadd.length > 80) {
1443 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1448 currentElementChild = currentElement.childNodes.item(i);
1454 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1456 // Recursively traverse the tree structure of the child node
1457 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1458 lastnode = currentElementChild.nodeName;
1460 currentElementChild=currentElement.childNodes.item(i);
1466 // The remaining code is mostly for formatting the tree
1467 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1472 ret+= "</"+tagName+">";
1478 applyBlacklists : function()
1480 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1481 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1485 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1486 if (b.indexOf(tag) > -1) {
1489 this.white.push(tag);
1493 Roo.each(w, function(tag) {
1494 if (b.indexOf(tag) > -1) {
1497 if (this.white.indexOf(tag) > -1) {
1500 this.white.push(tag);
1505 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1506 if (w.indexOf(tag) > -1) {
1509 this.black.push(tag);
1513 Roo.each(b, function(tag) {
1514 if (w.indexOf(tag) > -1) {
1517 if (this.black.indexOf(tag) > -1) {
1520 this.black.push(tag);
1525 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1526 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1530 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1531 if (b.indexOf(tag) > -1) {
1534 this.cwhite.push(tag);
1538 Roo.each(w, function(tag) {
1539 if (b.indexOf(tag) > -1) {
1542 if (this.cwhite.indexOf(tag) > -1) {
1545 this.cwhite.push(tag);
1550 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1551 if (w.indexOf(tag) > -1) {
1554 this.cblack.push(tag);
1558 Roo.each(b, function(tag) {
1559 if (w.indexOf(tag) > -1) {
1562 if (this.cblack.indexOf(tag) > -1) {
1565 this.cblack.push(tag);
1570 setStylesheets : function(stylesheets)
1572 if(typeof(stylesheets) == 'string'){
1573 Roo.get(this.iframe.contentDocument.head).createChild({
1584 Roo.each(stylesheets, function(s) {
1589 Roo.get(_this.iframe.contentDocument.head).createChild({
1600 removeStylesheets : function()
1604 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1609 setStyle : function(style)
1611 Roo.get(this.iframe.contentDocument.head).createChild({
1620 // hide stuff that is not compatible
1638 * @cfg {String} fieldClass @hide
1641 * @cfg {String} focusClass @hide
1644 * @cfg {String} autoCreate @hide
1647 * @cfg {String} inputType @hide
1650 * @cfg {String} invalidClass @hide
1653 * @cfg {String} invalidText @hide
1656 * @cfg {String} msgFx @hide
1659 * @cfg {String} validateOnBlur @hide
1663 Roo.HtmlEditorCore.white = [
1664 'area', 'br', 'img', 'input', 'hr', 'wbr',
1666 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1667 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1668 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1669 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1670 'table', 'ul', 'xmp',
1672 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1675 'dir', 'menu', 'ol', 'ul', 'dl',
1681 Roo.HtmlEditorCore.black = [
1682 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1684 'base', 'basefont', 'bgsound', 'blink', 'body',
1685 'frame', 'frameset', 'head', 'html', 'ilayer',
1686 'iframe', 'layer', 'link', 'meta', 'object',
1687 'script', 'style' ,'title', 'xml' // clean later..
1689 Roo.HtmlEditorCore.clean = [
1690 'script', 'style', 'title', 'xml'
1692 Roo.HtmlEditorCore.remove = [
1697 Roo.HtmlEditorCore.ablack = [
1701 Roo.HtmlEditorCore.aclean = [
1702 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1706 Roo.HtmlEditorCore.pwhite= [
1707 'http', 'https', 'mailto'
1710 // white listed style attributes.
1711 Roo.HtmlEditorCore.cwhite= [
1712 // 'text-align', /// default is to allow most things..
1718 // black listed style attributes.
1719 Roo.HtmlEditorCore.cblack= [
1720 // 'font-size' -- this can be set by the project
1724 Roo.HtmlEditorCore.swapCodes =[