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 for (var i in this.stylesheets) {
165 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
170 st += '<style type="text/css">' +
171 'IMG { cursor: pointer } ' +
174 var cls = 'roo-htmleditor-body';
176 if(this.bodyCls.length){
177 cls += ' ' + this.bodyCls;
180 return '<html><head>' + st +
181 //<style type="text/css">' +
182 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
184 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
188 onRender : function(ct, position)
191 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
192 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
195 this.el.dom.style.border = '0 none';
196 this.el.dom.setAttribute('tabIndex', -1);
197 this.el.addClass('x-hidden hide');
201 if(Roo.isIE){ // fix IE 1px bogus margin
202 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
206 this.frameId = Roo.id();
210 var iframe = this.owner.wrap.createChild({
212 cls: 'form-control', // bootstrap..
216 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
221 this.iframe = iframe.dom;
225 this.doc.designMode = 'on';
228 this.doc.write(this.getDocMarkup());
232 var task = { // must defer to wait for browser to be ready
234 //console.log("run task?" + this.doc.readyState);
236 if(this.doc.body || this.doc.readyState == 'complete'){
238 this.doc.designMode="on";
242 Roo.TaskMgr.stop(task);
243 this.initEditor.defer(10, this);
250 Roo.TaskMgr.start(task);
255 onResize : function(w, h)
257 Roo.log('resize: ' +w + ',' + h );
258 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
262 if(typeof w == 'number'){
264 this.iframe.style.width = w + 'px';
266 if(typeof h == 'number'){
268 this.iframe.style.height = h + 'px';
270 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
277 * Toggles the editor between standard and source edit mode.
278 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
280 toggleSourceEdit : function(sourceEditMode){
282 this.sourceEditMode = sourceEditMode === true;
284 if(this.sourceEditMode){
286 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
289 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
290 //this.iframe.className = '';
293 //this.setSize(this.owner.wrap.getSize());
294 //this.fireEvent('editmodechange', this, this.sourceEditMode);
301 * Protected method that will not generally be called directly. If you need/want
302 * custom HTML cleanup, this is the method you should override.
303 * @param {String} html The HTML to be cleaned
304 * return {String} The cleaned HTML
306 cleanHtml : function(html){
309 if(Roo.isSafari){ // strip safari nonsense
310 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
313 if(html == ' '){
320 * HTML Editor -> Textarea
321 * Protected method that will not generally be called directly. Syncs the contents
322 * of the editor iframe with the textarea.
324 syncValue : function(){
325 if(this.initialized){
326 var bd = (this.doc.body || this.doc.documentElement);
327 //this.cleanUpPaste(); -- this is done else where and causes havoc..
328 var html = bd.innerHTML;
330 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
331 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
333 html = '<div style="'+m[0]+'">' + html + '</div>';
336 html = this.cleanHtml(html);
337 // fix up the special chars.. normaly like back quotes in word...
338 // however we do not want to do this with chinese..
339 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
341 var cc = match.charCodeAt();
343 // Get the character value, handling surrogate pairs
344 if (match.length == 2) {
345 // It's a surrogate pair, calculate the Unicode code point
346 var high = match.charCodeAt(0) - 0xD800;
347 var low = match.charCodeAt(1) - 0xDC00;
348 cc = (high * 0x400) + low + 0x10000;
350 (cc >= 0x4E00 && cc < 0xA000 ) ||
351 (cc >= 0x3400 && cc < 0x4E00 ) ||
352 (cc >= 0xf900 && cc < 0xfb00 )
357 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
358 return "&#" + cc + ";";
365 if(this.owner.fireEvent('beforesync', this, html) !== false){
366 this.el.dom.value = html;
367 this.owner.fireEvent('sync', this, html);
373 * Protected method that will not generally be called directly. Pushes the value of the textarea
374 * into the iframe editor.
376 pushValue : function(){
377 if(this.initialized){
378 var v = this.el.dom.value.trim();
384 if(this.owner.fireEvent('beforepush', this, v) !== false){
385 var d = (this.doc.body || this.doc.documentElement);
388 this.el.dom.value = d.innerHTML;
389 this.owner.fireEvent('push', this, v);
395 deferFocus : function(){
396 this.focus.defer(10, this);
401 if(this.win && !this.sourceEditMode){
408 assignDocWin: function()
410 var iframe = this.iframe;
413 this.doc = iframe.contentWindow.document;
414 this.win = iframe.contentWindow;
416 // if (!Roo.get(this.frameId)) {
419 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
420 // this.win = Roo.get(this.frameId).dom.contentWindow;
422 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
426 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
427 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
432 initEditor : function(){
433 //console.log("INIT EDITOR");
438 this.doc.designMode="on";
440 this.doc.write(this.getDocMarkup());
443 var dbody = (this.doc.body || this.doc.documentElement);
444 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
445 // this copies styles from the containing element into thsi one..
446 // not sure why we need all of this..
447 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
449 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
450 //ss['background-attachment'] = 'fixed'; // w3c
451 dbody.bgProperties = 'fixed'; // ie
452 //Roo.DomHelper.applyStyles(dbody, ss);
453 Roo.EventManager.on(this.doc, {
454 //'mousedown': this.onEditorEvent,
455 'mouseup': this.onEditorEvent,
456 'dblclick': this.onEditorEvent,
457 'click': this.onEditorEvent,
458 'keyup': this.onEditorEvent,
463 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
465 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
466 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
468 this.initialized = true;
470 this.owner.fireEvent('initialize', this);
475 onDestroy : function(){
481 //for (var i =0; i < this.toolbars.length;i++) {
482 // // fixme - ask toolbars for heights?
483 // this.toolbars[i].onDestroy();
486 //this.wrap.dom.innerHTML = '';
487 //this.wrap.remove();
492 onFirstFocus : function(){
497 this.activated = true;
500 if(Roo.isGecko){ // prevent silly gecko errors
502 var s = this.win.getSelection();
503 if(!s.focusNode || s.focusNode.nodeType != 3){
504 var r = s.getRangeAt(0);
505 r.selectNodeContents((this.doc.body || this.doc.documentElement));
510 this.execCmd('useCSS', true);
511 this.execCmd('styleWithCSS', false);
514 this.owner.fireEvent('activate', this);
518 adjustFont: function(btn){
519 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
520 //if(Roo.isSafari){ // safari
523 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
524 if(Roo.isSafari){ // safari
525 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
526 v = (v < 10) ? 10 : v;
527 v = (v > 48) ? 48 : v;
528 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
533 v = Math.max(1, v+adjust);
535 this.execCmd('FontSize', v );
538 onEditorEvent : function(e)
540 this.owner.fireEvent('editorevent', this, e);
541 // this.updateToolbar();
542 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
545 insertTag : function(tg)
547 // could be a bit smarter... -> wrap the current selected tRoo..
548 if (tg.toLowerCase() == 'span' ||
549 tg.toLowerCase() == 'code' ||
550 tg.toLowerCase() == 'sup' ||
551 tg.toLowerCase() == 'sub'
554 range = this.createRange(this.getSelection());
555 var wrappingNode = this.doc.createElement(tg.toLowerCase());
556 wrappingNode.appendChild(range.extractContents());
557 range.insertNode(wrappingNode);
564 this.execCmd("formatblock", tg);
568 insertText : function(txt)
572 var range = this.createRange();
573 range.deleteContents();
574 //alert(Sender.getAttribute('label'));
576 range.insertNode(this.doc.createTextNode(txt));
582 * Executes a Midas editor command on the editor document and performs necessary focus and
583 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
584 * @param {String} cmd The Midas command
585 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
587 relayCmd : function(cmd, value){
589 this.execCmd(cmd, value);
590 this.owner.fireEvent('editorevent', this);
591 //this.updateToolbar();
592 this.owner.deferFocus();
596 * Executes a Midas editor command directly on the editor document.
597 * For visual commands, you should use {@link #relayCmd} instead.
598 * <b>This should only be called after the editor is initialized.</b>
599 * @param {String} cmd The Midas command
600 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
602 execCmd : function(cmd, value){
603 this.doc.execCommand(cmd, false, value === undefined ? null : value);
610 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
612 * @param {String} text | dom node..
614 insertAtCursor : function(text)
623 var r = this.doc.selection.createRange();
634 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
638 // from jquery ui (MIT licenced)
642 if (win.getSelection && win.getSelection().getRangeAt) {
643 range = win.getSelection().getRangeAt(0);
644 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
645 range.insertNode(node);
646 } else if (win.document.selection && win.document.selection.createRange) {
647 // no firefox support
648 var txt = typeof(text) == 'string' ? text : text.outerHTML;
649 win.document.selection.createRange().pasteHTML(txt);
651 // no firefox support
652 var txt = typeof(text) == 'string' ? text : text.outerHTML;
653 this.execCmd('InsertHTML', txt);
662 mozKeyPress : function(e){
664 var c = e.getCharCode(), cmd;
667 c = String.fromCharCode(c).toLowerCase();
681 this.cleanUpPaste.defer(100, this);
697 fixKeys : function(){ // load time branching for fastest keydown performance
700 var k = e.getKey(), r;
703 r = this.doc.selection.createRange();
706 r.pasteHTML('    ');
713 r = this.doc.selection.createRange();
715 var target = r.parentElement();
716 if(!target || target.tagName.toLowerCase() != 'li'){
718 r.pasteHTML('<br />');
724 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
725 this.cleanUpPaste.defer(100, this);
731 }else if(Roo.isOpera){
737 this.execCmd('InsertHTML','    ');
740 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
741 this.cleanUpPaste.defer(100, this);
746 }else if(Roo.isSafari){
752 this.execCmd('InsertText','\t');
756 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
757 this.cleanUpPaste.defer(100, this);
765 getAllAncestors: function()
767 var p = this.getSelectedNode();
770 a.push(p); // push blank onto stack..
771 p = this.getParentElement();
775 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
779 a.push(this.doc.body);
786 getSelection : function()
789 return Roo.isIE ? this.doc.selection : this.win.getSelection();
792 getSelectedNode: function()
794 // this may only work on Gecko!!!
796 // should we cache this!!!!
801 var range = this.createRange(this.getSelection()).cloneRange();
804 var parent = range.parentElement();
806 var testRange = range.duplicate();
807 testRange.moveToElementText(parent);
808 if (testRange.inRange(range)) {
811 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
814 parent = parent.parentElement;
819 // is ancestor a text element.
820 var ac = range.commonAncestorContainer;
821 if (ac.nodeType == 3) {
825 var ar = ac.childNodes;
828 var other_nodes = [];
829 var has_other_nodes = false;
830 for (var i=0;i<ar.length;i++) {
831 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
834 // fullly contained node.
836 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
841 // probably selected..
842 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
843 other_nodes.push(ar[i]);
847 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
852 has_other_nodes = true;
854 if (!nodes.length && other_nodes.length) {
857 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
863 createRange: function(sel)
865 // this has strange effects when using with
866 // top toolbar - not sure if it's a great idea.
867 //this.editor.contentWindow.focus();
868 if (typeof sel != "undefined") {
870 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
872 return this.doc.createRange();
875 return this.doc.createRange();
878 getParentElement: function()
882 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
884 var range = this.createRange(sel);
887 var p = range.commonAncestorContainer;
888 while (p.nodeType == 3) { // text node
899 * Range intersection.. the hard stuff...
903 * [ -- selected range --- ]
907 * if end is before start or hits it. fail.
908 * if start is after end or hits it fail.
910 * if either hits (but other is outside. - then it's not
916 // @see http://www.thismuchiknow.co.uk/?p=64.
917 rangeIntersectsNode : function(range, node)
919 var nodeRange = node.ownerDocument.createRange();
921 nodeRange.selectNode(node);
923 nodeRange.selectNodeContents(node);
926 var rangeStartRange = range.cloneRange();
927 rangeStartRange.collapse(true);
929 var rangeEndRange = range.cloneRange();
930 rangeEndRange.collapse(false);
932 var nodeStartRange = nodeRange.cloneRange();
933 nodeStartRange.collapse(true);
935 var nodeEndRange = nodeRange.cloneRange();
936 nodeEndRange.collapse(false);
938 return rangeStartRange.compareBoundaryPoints(
939 Range.START_TO_START, nodeEndRange) == -1 &&
940 rangeEndRange.compareBoundaryPoints(
941 Range.START_TO_START, nodeStartRange) == 1;
945 rangeCompareNode : function(range, node)
947 var nodeRange = node.ownerDocument.createRange();
949 nodeRange.selectNode(node);
951 nodeRange.selectNodeContents(node);
955 range.collapse(true);
957 nodeRange.collapse(true);
959 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
960 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
962 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
964 var nodeIsBefore = ss == 1;
965 var nodeIsAfter = ee == -1;
967 if (nodeIsBefore && nodeIsAfter) {
970 if (!nodeIsBefore && nodeIsAfter) {
971 return 1; //right trailed.
974 if (nodeIsBefore && !nodeIsAfter) {
975 return 2; // left trailed.
981 // private? - in a new class?
982 cleanUpPaste : function()
984 // cleans up the whole document..
985 Roo.log('cleanuppaste');
987 this.cleanUpChildren(this.doc.body);
988 var clean = this.cleanWordChars(this.doc.body.innerHTML);
989 if (clean != this.doc.body.innerHTML) {
990 this.doc.body.innerHTML = clean;
995 cleanWordChars : function(input) {// change the chars to hex code
996 var he = Roo.HtmlEditorCore;
999 Roo.each(he.swapCodes, function(sw) {
1000 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1002 output = output.replace(swapper, sw[1]);
1009 cleanUpChildren : function (n)
1011 if (!n.childNodes.length) {
1014 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1015 this.cleanUpChild(n.childNodes[i]);
1022 cleanUpChild : function (node)
1025 //console.log(node);
1026 if (node.nodeName == "#text") {
1027 // clean up silly Windows -- stuff?
1030 if (node.nodeName == "#comment") {
1031 node.parentNode.removeChild(node);
1032 // clean up silly Windows -- stuff?
1035 var lcname = node.tagName.toLowerCase();
1036 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1037 // whitelist of tags..
1039 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1041 node.parentNode.removeChild(node);
1046 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1048 // spans with no attributes - just remove them..
1049 if ((!node.attributes || !node.attributes.length) && lcname == 'span') {
1050 remove_keep_children = true;
1053 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1054 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1056 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1057 // remove_keep_children = true;
1060 if (remove_keep_children) {
1061 this.cleanUpChildren(node);
1062 // inserts everything just before this node...
1063 while (node.childNodes.length) {
1064 var cn = node.childNodes[0];
1065 node.removeChild(cn);
1066 node.parentNode.insertBefore(cn, node);
1068 node.parentNode.removeChild(node);
1072 if (!node.attributes || !node.attributes.length) {
1077 this.cleanUpChildren(node);
1081 function cleanAttr(n,v)
1084 if (v.match(/^\./) || v.match(/^\//)) {
1087 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1090 if (v.match(/^#/)) {
1093 if (v.match(/^\{/)) { // allow template editing.
1096 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1097 node.removeAttribute(n);
1101 var cwhite = this.cwhite;
1102 var cblack = this.cblack;
1104 function cleanStyle(n,v)
1106 if (v.match(/expression/)) { //XSS?? should we even bother..
1107 node.removeAttribute(n);
1111 var parts = v.split(/;/);
1114 Roo.each(parts, function(p) {
1115 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1119 var l = p.split(':').shift().replace(/\s+/g,'');
1120 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1122 if ( cwhite.length && cblack.indexOf(l) > -1) {
1123 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1124 //node.removeAttribute(n);
1128 // only allow 'c whitelisted system attributes'
1129 if ( cwhite.length && cwhite.indexOf(l) < 0) {
1130 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1131 //node.removeAttribute(n);
1142 node.setAttribute(n, clean.join(';'));
1144 node.removeAttribute(n);
1150 for (var i = node.attributes.length-1; i > -1 ; i--) {
1151 var a = node.attributes[i];
1154 if (a.name.toLowerCase().substr(0,2)=='on') {
1155 node.removeAttribute(a.name);
1158 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1159 node.removeAttribute(a.name);
1162 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1163 cleanAttr(a.name,a.value); // fixme..
1166 if (a.name == 'style') {
1167 cleanStyle(a.name,a.value);
1170 /// clean up MS crap..
1171 // tecnically this should be a list of valid class'es..
1174 if (a.name == 'class') {
1175 if (a.value.match(/^Mso/)) {
1176 node.removeAttribute('class');
1179 if (a.value.match(/^body$/)) {
1180 node.removeAttribute('class');
1191 this.cleanUpChildren(node);
1197 * Clean up MS wordisms...
1199 cleanWord : function(node)
1202 this.cleanWord(this.doc.body);
1207 node.nodeName == 'SPAN' &&
1208 !node.hasAttributes() &&
1209 node.childNodes.length == 1 &&
1210 node.firstChild.nodeName == "#text"
1212 var textNode = node.firstChild;
1213 node.removeChild(textNode);
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.insertBefore(textNode, node);
1218 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1219 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1221 node.parentNode.removeChild(node);
1224 if (node.nodeName == "#text") {
1225 // clean up silly Windows -- stuff?
1228 if (node.nodeName == "#comment") {
1229 node.parentNode.removeChild(node);
1230 // clean up silly Windows -- stuff?
1234 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1235 node.parentNode.removeChild(node);
1238 //Roo.log(node.tagName);
1239 // remove - but keep children..
1240 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|v:|font)/)) {
1241 //Roo.log('-- removed');
1242 while (node.childNodes.length) {
1243 var cn = node.childNodes[0];
1244 node.removeChild(cn);
1245 node.parentNode.insertBefore(cn, node);
1246 // move node to parent - and clean it..
1249 node.parentNode.removeChild(node);
1250 /// no need to iterate chidlren = it's got none..
1251 //this.iterateChildren(node, this.cleanWord);
1255 if (node.className.length) {
1257 var cn = node.className.split(/\W+/);
1259 Roo.each(cn, function(cls) {
1260 if (cls.match(/Mso[a-zA-Z]+/)) {
1265 node.className = cna.length ? cna.join(' ') : '';
1267 node.removeAttribute("class");
1271 if (node.hasAttribute("lang")) {
1272 node.removeAttribute("lang");
1275 if (node.hasAttribute("style")) {
1277 var styles = node.getAttribute("style").split(";");
1279 Roo.each(styles, function(s) {
1280 if (!s.match(/:/)) {
1283 var kv = s.split(":");
1284 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1287 // what ever is left... we allow.
1290 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1291 if (!nstyle.length) {
1292 node.removeAttribute('style');
1295 this.iterateChildren(node, this.cleanWord);
1301 * iterateChildren of a Node, calling fn each time, using this as the scole..
1302 * @param {DomNode} node node to iterate children of.
1303 * @param {Function} fn method of this class to call on each item.
1305 iterateChildren : function(node, fn)
1307 if (!node.childNodes.length) {
1310 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1311 fn.call(this, node.childNodes[i])
1319 * Quite often pasting from word etc.. results in tables with column and widths.
1320 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1323 cleanTableWidths : function(node)
1328 this.cleanTableWidths(this.doc.body);
1333 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1336 Roo.log(node.tagName);
1337 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1338 this.iterateChildren(node, this.cleanTableWidths);
1341 if (node.hasAttribute('width')) {
1342 node.removeAttribute('width');
1346 if (node.hasAttribute("style")) {
1349 var styles = node.getAttribute("style").split(";");
1351 Roo.each(styles, function(s) {
1352 if (!s.match(/:/)) {
1355 var kv = s.split(":");
1356 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1359 // what ever is left... we allow.
1362 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1363 if (!nstyle.length) {
1364 node.removeAttribute('style');
1368 this.iterateChildren(node, this.cleanTableWidths);
1376 domToHTML : function(currentElement, depth, nopadtext) {
1379 nopadtext = nopadtext || false;
1381 if (!currentElement) {
1382 return this.domToHTML(this.doc.body);
1385 //Roo.log(currentElement);
1387 var allText = false;
1388 var nodeName = currentElement.nodeName;
1389 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1391 if (nodeName == '#text') {
1393 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1398 if (nodeName != 'BODY') {
1401 // Prints the node tagName, such as <A>, <IMG>, etc
1404 for(i = 0; i < currentElement.attributes.length;i++) {
1406 var aname = currentElement.attributes.item(i).name;
1407 if (!currentElement.attributes.item(i).value.length) {
1410 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1413 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1422 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1425 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1430 // Traverse the tree
1432 var currentElementChild = currentElement.childNodes.item(i);
1436 while (currentElementChild) {
1437 // Formatting code (indent the tree so it looks nice on the screen)
1438 var nopad = nopadtext;
1439 if (lastnode == 'SPAN') {
1443 if (currentElementChild.nodeName == '#text') {
1444 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1445 toadd = nopadtext ? toadd : toadd.trim();
1446 if (!nopad && toadd.length > 80) {
1447 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1452 currentElementChild = currentElement.childNodes.item(i);
1458 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1460 // Recursively traverse the tree structure of the child node
1461 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1462 lastnode = currentElementChild.nodeName;
1464 currentElementChild=currentElement.childNodes.item(i);
1470 // The remaining code is mostly for formatting the tree
1471 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1476 ret+= "</"+tagName+">";
1482 applyBlacklists : function()
1484 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1485 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1489 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1490 if (b.indexOf(tag) > -1) {
1493 this.white.push(tag);
1497 Roo.each(w, function(tag) {
1498 if (b.indexOf(tag) > -1) {
1501 if (this.white.indexOf(tag) > -1) {
1504 this.white.push(tag);
1509 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1510 if (w.indexOf(tag) > -1) {
1513 this.black.push(tag);
1517 Roo.each(b, function(tag) {
1518 if (w.indexOf(tag) > -1) {
1521 if (this.black.indexOf(tag) > -1) {
1524 this.black.push(tag);
1529 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1530 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1534 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1535 if (b.indexOf(tag) > -1) {
1538 this.cwhite.push(tag);
1542 Roo.each(w, function(tag) {
1543 if (b.indexOf(tag) > -1) {
1546 if (this.cwhite.indexOf(tag) > -1) {
1549 this.cwhite.push(tag);
1554 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1555 if (w.indexOf(tag) > -1) {
1558 this.cblack.push(tag);
1562 Roo.each(b, function(tag) {
1563 if (w.indexOf(tag) > -1) {
1566 if (this.cblack.indexOf(tag) > -1) {
1569 this.cblack.push(tag);
1574 setStylesheets : function(stylesheets)
1576 if(typeof(stylesheets) == 'string'){
1577 Roo.get(this.iframe.contentDocument.head).createChild({
1588 Roo.each(stylesheets, function(s) {
1593 Roo.get(_this.iframe.contentDocument.head).createChild({
1604 removeStylesheets : function()
1608 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1613 setStyle : function(style)
1615 Roo.get(this.iframe.contentDocument.head).createChild({
1624 // hide stuff that is not compatible
1642 * @cfg {String} fieldClass @hide
1645 * @cfg {String} focusClass @hide
1648 * @cfg {String} autoCreate @hide
1651 * @cfg {String} inputType @hide
1654 * @cfg {String} invalidClass @hide
1657 * @cfg {String} invalidText @hide
1660 * @cfg {String} msgFx @hide
1663 * @cfg {String} validateOnBlur @hide
1667 Roo.HtmlEditorCore.white = [
1668 'area', 'br', 'img', 'input', 'hr', 'wbr',
1670 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1671 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1672 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1673 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1674 'table', 'ul', 'xmp',
1676 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1679 'dir', 'menu', 'ol', 'ul', 'dl',
1685 Roo.HtmlEditorCore.black = [
1686 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1688 'base', 'basefont', 'bgsound', 'blink', 'body',
1689 'frame', 'frameset', 'head', 'html', 'ilayer',
1690 'iframe', 'layer', 'link', 'meta', 'object',
1691 'script', 'style' ,'title', 'xml' // clean later..
1693 Roo.HtmlEditorCore.clean = [
1694 'script', 'style', 'title', 'xml'
1696 Roo.HtmlEditorCore.remove = [
1701 Roo.HtmlEditorCore.ablack = [
1705 Roo.HtmlEditorCore.aclean = [
1706 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1710 Roo.HtmlEditorCore.pwhite= [
1711 'http', 'https', 'mailto'
1714 // white listed style attributes.
1715 Roo.HtmlEditorCore.cwhite= [
1716 // 'text-align', /// default is to allow most things..
1722 // black listed style attributes.
1723 Roo.HtmlEditorCore.cblack= [
1724 // 'font-size' -- this can be set by the project
1728 Roo.HtmlEditorCore.swapCodes =[