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.
118 * @cfg {boolean} allowComments - default false - allow comments in HTML source - by default they are stripped - if you are editing email you may need this.
120 allowComments: false,
124 // private properties
125 validationEvent : false,
129 sourceEditMode : false,
130 onFocus : Roo.emptyFn,
136 // blacklist + whitelisted elements..
143 * Protected method that will not generally be called directly. It
144 * is called when the editor initializes the iframe with HTML contents. Override this method if you
145 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
147 getDocMarkup : function(){
151 // inherit styels from page...??
152 if (this.stylesheets === false) {
154 Roo.get(document.head).select('style').each(function(node) {
155 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
158 Roo.get(document.head).select('link').each(function(node) {
159 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
162 } else if (!this.stylesheets.length) {
164 st = '<style type="text/css">' +
165 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
168 for (var i in this.stylesheets) {
169 if (typeof(this.stylesheets[i]) != 'string') {
172 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
177 st += '<style type="text/css">' +
178 'IMG { cursor: pointer } ' +
181 var cls = 'roo-htmleditor-body';
183 if(this.bodyCls.length){
184 cls += ' ' + this.bodyCls;
187 return '<html><head>' + st +
188 //<style type="text/css">' +
189 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
191 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
195 onRender : function(ct, position)
198 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
199 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
202 this.el.dom.style.border = '0 none';
203 this.el.dom.setAttribute('tabIndex', -1);
204 this.el.addClass('x-hidden hide');
208 if(Roo.isIE){ // fix IE 1px bogus margin
209 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
213 this.frameId = Roo.id();
217 var iframe = this.owner.wrap.createChild({
219 cls: 'form-control', // bootstrap..
223 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
228 this.iframe = iframe.dom;
232 this.doc.designMode = 'on';
235 this.doc.write(this.getDocMarkup());
239 var task = { // must defer to wait for browser to be ready
241 //console.log("run task?" + this.doc.readyState);
243 if(this.doc.body || this.doc.readyState == 'complete'){
245 this.doc.designMode="on";
249 Roo.TaskMgr.stop(task);
250 this.initEditor.defer(10, this);
257 Roo.TaskMgr.start(task);
262 onResize : function(w, h)
264 Roo.log('resize: ' +w + ',' + h );
265 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
269 if(typeof w == 'number'){
271 this.iframe.style.width = w + 'px';
273 if(typeof h == 'number'){
275 this.iframe.style.height = h + 'px';
277 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
284 * Toggles the editor between standard and source edit mode.
285 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
287 toggleSourceEdit : function(sourceEditMode){
289 this.sourceEditMode = sourceEditMode === true;
291 if(this.sourceEditMode){
293 Roo.get(this.iframe).addClass(['x-hidden','hide']); //FIXME - what's the BS styles for these
296 Roo.get(this.iframe).removeClass(['x-hidden','hide']);
297 //this.iframe.className = '';
300 //this.setSize(this.owner.wrap.getSize());
301 //this.fireEvent('editmodechange', this, this.sourceEditMode);
308 * Protected method that will not generally be called directly. If you need/want
309 * custom HTML cleanup, this is the method you should override.
310 * @param {String} html The HTML to be cleaned
311 * return {String} The cleaned HTML
313 cleanHtml : function(html){
316 if(Roo.isSafari){ // strip safari nonsense
317 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
320 if(html == ' '){
327 * HTML Editor -> Textarea
328 * Protected method that will not generally be called directly. Syncs the contents
329 * of the editor iframe with the textarea.
331 syncValue : function(){
332 if(this.initialized){
333 var bd = (this.doc.body || this.doc.documentElement);
334 //this.cleanUpPaste(); -- this is done else where and causes havoc..
335 var html = bd.innerHTML;
337 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
338 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
340 html = '<div style="'+m[0]+'">' + html + '</div>';
343 html = this.cleanHtml(html);
344 // fix up the special chars.. normaly like back quotes in word...
345 // however we do not want to do this with chinese..
346 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
348 var cc = match.charCodeAt();
350 // Get the character value, handling surrogate pairs
351 if (match.length == 2) {
352 // It's a surrogate pair, calculate the Unicode code point
353 var high = match.charCodeAt(0) - 0xD800;
354 var low = match.charCodeAt(1) - 0xDC00;
355 cc = (high * 0x400) + low + 0x10000;
357 (cc >= 0x4E00 && cc < 0xA000 ) ||
358 (cc >= 0x3400 && cc < 0x4E00 ) ||
359 (cc >= 0xf900 && cc < 0xfb00 )
364 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
365 return "&#" + cc + ";";
372 if(this.owner.fireEvent('beforesync', this, html) !== false){
373 this.el.dom.value = html;
374 this.owner.fireEvent('sync', this, html);
380 * Protected method that will not generally be called directly. Pushes the value of the textarea
381 * into the iframe editor.
383 pushValue : function(){
384 if(this.initialized){
385 var v = this.el.dom.value.trim();
391 if(this.owner.fireEvent('beforepush', this, v) !== false){
392 var d = (this.doc.body || this.doc.documentElement);
395 this.el.dom.value = d.innerHTML;
396 this.owner.fireEvent('push', this, v);
402 deferFocus : function(){
403 this.focus.defer(10, this);
408 if(this.win && !this.sourceEditMode){
415 assignDocWin: function()
417 var iframe = this.iframe;
420 this.doc = iframe.contentWindow.document;
421 this.win = iframe.contentWindow;
423 // if (!Roo.get(this.frameId)) {
426 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
427 // this.win = Roo.get(this.frameId).dom.contentWindow;
429 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
433 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
434 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
439 initEditor : function(){
440 //console.log("INIT EDITOR");
445 this.doc.designMode="on";
447 this.doc.write(this.getDocMarkup());
450 var dbody = (this.doc.body || this.doc.documentElement);
451 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
452 // this copies styles from the containing element into thsi one..
453 // not sure why we need all of this..
454 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
456 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
457 //ss['background-attachment'] = 'fixed'; // w3c
458 dbody.bgProperties = 'fixed'; // ie
459 //Roo.DomHelper.applyStyles(dbody, ss);
460 Roo.EventManager.on(this.doc, {
461 //'mousedown': this.onEditorEvent,
462 'mouseup': this.onEditorEvent,
463 'dblclick': this.onEditorEvent,
464 'click': this.onEditorEvent,
465 'keyup': this.onEditorEvent,
470 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
472 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
473 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
475 this.initialized = true;
477 this.owner.fireEvent('initialize', this);
482 onDestroy : function(){
488 //for (var i =0; i < this.toolbars.length;i++) {
489 // // fixme - ask toolbars for heights?
490 // this.toolbars[i].onDestroy();
493 //this.wrap.dom.innerHTML = '';
494 //this.wrap.remove();
499 onFirstFocus : function(){
504 this.activated = true;
507 if(Roo.isGecko){ // prevent silly gecko errors
509 var s = this.win.getSelection();
510 if(!s.focusNode || s.focusNode.nodeType != 3){
511 var r = s.getRangeAt(0);
512 r.selectNodeContents((this.doc.body || this.doc.documentElement));
517 this.execCmd('useCSS', true);
518 this.execCmd('styleWithCSS', false);
521 this.owner.fireEvent('activate', this);
525 adjustFont: function(btn){
526 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
527 //if(Roo.isSafari){ // safari
530 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
531 if(Roo.isSafari){ // safari
532 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
533 v = (v < 10) ? 10 : v;
534 v = (v > 48) ? 48 : v;
535 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
540 v = Math.max(1, v+adjust);
542 this.execCmd('FontSize', v );
545 onEditorEvent : function(e)
547 this.owner.fireEvent('editorevent', this, e);
548 // this.updateToolbar();
549 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
552 insertTag : function(tg)
554 // could be a bit smarter... -> wrap the current selected tRoo..
555 if (tg.toLowerCase() == 'span' ||
556 tg.toLowerCase() == 'code' ||
557 tg.toLowerCase() == 'sup' ||
558 tg.toLowerCase() == 'sub'
561 range = this.createRange(this.getSelection());
562 var wrappingNode = this.doc.createElement(tg.toLowerCase());
563 wrappingNode.appendChild(range.extractContents());
564 range.insertNode(wrappingNode);
571 this.execCmd("formatblock", tg);
575 insertText : function(txt)
579 var range = this.createRange();
580 range.deleteContents();
581 //alert(Sender.getAttribute('label'));
583 range.insertNode(this.doc.createTextNode(txt));
589 * Executes a Midas editor command on the editor document and performs necessary focus and
590 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
591 * @param {String} cmd The Midas command
592 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
594 relayCmd : function(cmd, value){
596 this.execCmd(cmd, value);
597 this.owner.fireEvent('editorevent', this);
598 //this.updateToolbar();
599 this.owner.deferFocus();
603 * Executes a Midas editor command directly on the editor document.
604 * For visual commands, you should use {@link #relayCmd} instead.
605 * <b>This should only be called after the editor is initialized.</b>
606 * @param {String} cmd The Midas command
607 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
609 execCmd : function(cmd, value){
610 this.doc.execCommand(cmd, false, value === undefined ? null : value);
617 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
619 * @param {String} text | dom node..
621 insertAtCursor : function(text)
630 var r = this.doc.selection.createRange();
641 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
645 // from jquery ui (MIT licenced)
649 if (win.getSelection && win.getSelection().getRangeAt) {
650 range = win.getSelection().getRangeAt(0);
651 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
652 range.insertNode(node);
653 } else if (win.document.selection && win.document.selection.createRange) {
654 // no firefox support
655 var txt = typeof(text) == 'string' ? text : text.outerHTML;
656 win.document.selection.createRange().pasteHTML(txt);
658 // no firefox support
659 var txt = typeof(text) == 'string' ? text : text.outerHTML;
660 this.execCmd('InsertHTML', txt);
669 mozKeyPress : function(e){
671 var c = e.getCharCode(), cmd;
674 c = String.fromCharCode(c).toLowerCase();
688 this.cleanUpPaste.defer(100, this);
704 fixKeys : function(){ // load time branching for fastest keydown performance
707 var k = e.getKey(), r;
710 r = this.doc.selection.createRange();
713 r.pasteHTML('    ');
720 r = this.doc.selection.createRange();
722 var target = r.parentElement();
723 if(!target || target.tagName.toLowerCase() != 'li'){
725 r.pasteHTML('<br />');
731 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
732 this.cleanUpPaste.defer(100, this);
738 }else if(Roo.isOpera){
744 this.execCmd('InsertHTML','    ');
747 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
748 this.cleanUpPaste.defer(100, this);
753 }else if(Roo.isSafari){
759 this.execCmd('InsertText','\t');
763 if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
764 this.cleanUpPaste.defer(100, this);
772 getAllAncestors: function()
774 var p = this.getSelectedNode();
777 a.push(p); // push blank onto stack..
778 p = this.getParentElement();
782 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
786 a.push(this.doc.body);
793 getSelection : function()
796 return Roo.isIE ? this.doc.selection : this.win.getSelection();
799 getSelectedNode: function()
801 // this may only work on Gecko!!!
803 // should we cache this!!!!
808 var range = this.createRange(this.getSelection()).cloneRange();
811 var parent = range.parentElement();
813 var testRange = range.duplicate();
814 testRange.moveToElementText(parent);
815 if (testRange.inRange(range)) {
818 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
821 parent = parent.parentElement;
826 // is ancestor a text element.
827 var ac = range.commonAncestorContainer;
828 if (ac.nodeType == 3) {
832 var ar = ac.childNodes;
835 var other_nodes = [];
836 var has_other_nodes = false;
837 for (var i=0;i<ar.length;i++) {
838 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
841 // fullly contained node.
843 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
848 // probably selected..
849 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
850 other_nodes.push(ar[i]);
854 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
859 has_other_nodes = true;
861 if (!nodes.length && other_nodes.length) {
864 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
870 createRange: function(sel)
872 // this has strange effects when using with
873 // top toolbar - not sure if it's a great idea.
874 //this.editor.contentWindow.focus();
875 if (typeof sel != "undefined") {
877 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
879 return this.doc.createRange();
882 return this.doc.createRange();
885 getParentElement: function()
889 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
891 var range = this.createRange(sel);
894 var p = range.commonAncestorContainer;
895 while (p.nodeType == 3) { // text node
906 * Range intersection.. the hard stuff...
910 * [ -- selected range --- ]
914 * if end is before start or hits it. fail.
915 * if start is after end or hits it fail.
917 * if either hits (but other is outside. - then it's not
923 // @see http://www.thismuchiknow.co.uk/?p=64.
924 rangeIntersectsNode : function(range, node)
926 var nodeRange = node.ownerDocument.createRange();
928 nodeRange.selectNode(node);
930 nodeRange.selectNodeContents(node);
933 var rangeStartRange = range.cloneRange();
934 rangeStartRange.collapse(true);
936 var rangeEndRange = range.cloneRange();
937 rangeEndRange.collapse(false);
939 var nodeStartRange = nodeRange.cloneRange();
940 nodeStartRange.collapse(true);
942 var nodeEndRange = nodeRange.cloneRange();
943 nodeEndRange.collapse(false);
945 return rangeStartRange.compareBoundaryPoints(
946 Range.START_TO_START, nodeEndRange) == -1 &&
947 rangeEndRange.compareBoundaryPoints(
948 Range.START_TO_START, nodeStartRange) == 1;
952 rangeCompareNode : function(range, node)
954 var nodeRange = node.ownerDocument.createRange();
956 nodeRange.selectNode(node);
958 nodeRange.selectNodeContents(node);
962 range.collapse(true);
964 nodeRange.collapse(true);
966 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
967 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
969 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
971 var nodeIsBefore = ss == 1;
972 var nodeIsAfter = ee == -1;
974 if (nodeIsBefore && nodeIsAfter) {
977 if (!nodeIsBefore && nodeIsAfter) {
978 return 1; //right trailed.
981 if (nodeIsBefore && !nodeIsAfter) {
982 return 2; // left trailed.
988 // private? - in a new class?
989 cleanUpPaste : function()
991 // cleans up the whole document..
992 Roo.log('cleanuppaste');
994 this.cleanUpChildren(this.doc.body);
995 var clean = this.cleanWordChars(this.doc.body.innerHTML);
996 if (clean != this.doc.body.innerHTML) {
997 this.doc.body.innerHTML = clean;
1002 cleanWordChars : function(input) {// change the chars to hex code
1003 var he = Roo.HtmlEditorCore;
1006 Roo.each(he.swapCodes, function(sw) {
1007 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1009 output = output.replace(swapper, sw[1]);
1016 cleanUpChildren : function (n)
1018 if (!n.childNodes.length) {
1021 for (var i = n.childNodes.length-1; i > -1 ; i--) {
1022 this.cleanUpChild(n.childNodes[i]);
1029 cleanUpChild : function (node)
1032 //console.log(node);
1033 if (node.nodeName == "#text") {
1034 // clean up silly Windows -- stuff?
1037 if (node.nodeName == "#comment") {
1038 if (!this.allowComments) {
1039 node.parentNode.removeChild(node);
1041 // clean up silly Windows -- stuff?
1044 var lcname = node.tagName.toLowerCase();
1045 // we ignore whitelists... ?? = not really the way to go, but we probably have not got a full
1046 // whitelist of tags..
1048 if (this.black.indexOf(lcname) > -1 && this.clearUp ) {
1050 node.parentNode.removeChild(node);
1055 var remove_keep_children= Roo.HtmlEditorCore.remove.indexOf(node.tagName.toLowerCase()) > -1;
1057 // spans with no attributes - just remove them..
1058 if ((!node.attributes || !node.attributes.length) && lcname == 'span') {
1059 remove_keep_children = true;
1062 // remove <a name=....> as rendering on yahoo mailer is borked with this.
1063 // this will have to be flaged elsewhere - perhaps ablack=name... on the mailer..
1065 //if (node.tagName.toLowerCase() == 'a' && !node.hasAttribute('href')) {
1066 // remove_keep_children = true;
1069 if (remove_keep_children) {
1070 this.cleanUpChildren(node);
1071 // inserts everything just before this node...
1072 while (node.childNodes.length) {
1073 var cn = node.childNodes[0];
1074 node.removeChild(cn);
1075 node.parentNode.insertBefore(cn, node);
1077 node.parentNode.removeChild(node);
1081 if (!node.attributes || !node.attributes.length) {
1086 this.cleanUpChildren(node);
1090 function cleanAttr(n,v)
1093 if (v.match(/^\./) || v.match(/^\//)) {
1096 if (v.match(/^(http|https):\/\//) || v.match(/^mailto:/) || v.match(/^ftp:/)) {
1099 if (v.match(/^#/)) {
1102 if (v.match(/^\{/)) { // allow template editing.
1105 // Roo.log("(REMOVE TAG)"+ node.tagName +'.' + n + '=' + v);
1106 node.removeAttribute(n);
1110 var cwhite = this.cwhite;
1111 var cblack = this.cblack;
1113 function cleanStyle(n,v)
1115 if (v.match(/expression/)) { //XSS?? should we even bother..
1116 node.removeAttribute(n);
1120 var parts = v.split(/;/);
1123 Roo.each(parts, function(p) {
1124 p = p.replace(/^\s+/g,'').replace(/\s+$/g,'');
1128 var l = p.split(':').shift().replace(/\s+/g,'');
1129 l = l.replace(/^\s+/g,'').replace(/\s+$/g,'');
1131 if ( cwhite.length && cblack.indexOf(l) > -1) {
1132 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1133 //node.removeAttribute(n);
1137 // only allow 'c whitelisted system attributes'
1138 if ( cwhite.length && cwhite.indexOf(l) < 0 && cwhite.indexOf(l.toLowerCase()) < 0 ) {
1139 // Roo.log('(REMOVE CSS)' + node.tagName +'.' + n + ':'+l + '=' + v);
1140 //node.removeAttribute(n);
1151 node.setAttribute(n, clean.join(';'));
1153 node.removeAttribute(n);
1159 for (var i = node.attributes.length-1; i > -1 ; i--) {
1160 var a = node.attributes[i];
1163 if (a.name.toLowerCase().substr(0,2)=='on') {
1164 node.removeAttribute(a.name);
1167 if (Roo.HtmlEditorCore.ablack.indexOf(a.name.toLowerCase()) > -1) {
1168 node.removeAttribute(a.name);
1171 if (Roo.HtmlEditorCore.aclean.indexOf(a.name.toLowerCase()) > -1) {
1172 cleanAttr(a.name,a.value); // fixme..
1175 if (a.name == 'style') {
1176 cleanStyle(a.name,a.value);
1179 /// clean up MS crap..
1180 // tecnically this should be a list of valid class'es..
1183 if (a.name == 'class') {
1184 if (a.value.match(/^Mso/)) {
1185 node.removeAttribute('class');
1188 if (a.value.match(/^body$/)) {
1189 node.removeAttribute('class');
1200 this.cleanUpChildren(node);
1206 * Clean up MS wordisms...
1208 cleanWord : function(node)
1211 this.cleanWord(this.doc.body);
1216 node.nodeName == 'SPAN' &&
1217 !node.hasAttributes() &&
1218 node.childNodes.length == 1 &&
1219 node.firstChild.nodeName == "#text"
1221 var textNode = node.firstChild;
1222 node.removeChild(textNode);
1223 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1224 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" "), node);
1226 node.parentNode.insertBefore(textNode, node);
1227 if (node.getAttribute('lang') != 'zh-CN') { // do not space pad on chinese characters..
1228 node.parentNode.insertBefore(node.ownerDocument.createTextNode(" ") , node);
1230 node.parentNode.removeChild(node);
1233 if (node.nodeName == "#text") {
1234 // clean up silly Windows -- stuff?
1237 if (node.nodeName == "#comment") {
1238 node.parentNode.removeChild(node);
1239 // clean up silly Windows -- stuff?
1243 if (node.tagName.toLowerCase().match(/^(style|script|applet|embed|noframes|noscript)$/)) {
1244 node.parentNode.removeChild(node);
1247 //Roo.log(node.tagName);
1248 // remove - but keep children..
1249 if (node.tagName.toLowerCase().match(/^(meta|link|\\?xml:|st1:|o:|v:|font)/)) {
1250 //Roo.log('-- removed');
1251 while (node.childNodes.length) {
1252 var cn = node.childNodes[0];
1253 node.removeChild(cn);
1254 node.parentNode.insertBefore(cn, node);
1255 // move node to parent - and clean it..
1258 node.parentNode.removeChild(node);
1259 /// no need to iterate chidlren = it's got none..
1260 //this.iterateChildren(node, this.cleanWord);
1264 if (node.className.length) {
1266 var cn = node.className.split(/\W+/);
1268 Roo.each(cn, function(cls) {
1269 if (cls.match(/Mso[a-zA-Z]+/)) {
1274 node.className = cna.length ? cna.join(' ') : '';
1276 node.removeAttribute("class");
1280 if (node.hasAttribute("lang")) {
1281 node.removeAttribute("lang");
1284 if (node.hasAttribute("style")) {
1286 var styles = node.getAttribute("style").split(";");
1288 Roo.each(styles, function(s) {
1289 if (!s.match(/:/)) {
1292 var kv = s.split(":");
1293 if (kv[0].match(/^(mso-|line|font|background|margin|padding|color)/)) {
1296 // what ever is left... we allow.
1299 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1300 if (!nstyle.length) {
1301 node.removeAttribute('style');
1304 this.iterateChildren(node, this.cleanWord);
1310 * iterateChildren of a Node, calling fn each time, using this as the scole..
1311 * @param {DomNode} node node to iterate children of.
1312 * @param {Function} fn method of this class to call on each item.
1314 iterateChildren : function(node, fn)
1316 if (!node.childNodes.length) {
1319 for (var i = node.childNodes.length-1; i > -1 ; i--) {
1320 fn.call(this, node.childNodes[i])
1328 * Quite often pasting from word etc.. results in tables with column and widths.
1329 * This does not work well on fluid HTML layouts - like emails. - so this code should hunt an destroy them..
1332 cleanTableWidths : function(node)
1337 this.cleanTableWidths(this.doc.body);
1342 if (node.nodeName == "#text" || node.nodeName == "#comment") {
1345 Roo.log(node.tagName);
1346 if (!node.tagName.toLowerCase().match(/^(table|td|tr)$/)) {
1347 this.iterateChildren(node, this.cleanTableWidths);
1350 if (node.hasAttribute('width')) {
1351 node.removeAttribute('width');
1355 if (node.hasAttribute("style")) {
1358 var styles = node.getAttribute("style").split(";");
1360 Roo.each(styles, function(s) {
1361 if (!s.match(/:/)) {
1364 var kv = s.split(":");
1365 if (kv[0].match(/^\s*(width|min-width)\s*$/)) {
1368 // what ever is left... we allow.
1371 node.setAttribute("style", nstyle.length ? nstyle.join(';') : '');
1372 if (!nstyle.length) {
1373 node.removeAttribute('style');
1377 this.iterateChildren(node, this.cleanTableWidths);
1385 domToHTML : function(currentElement, depth, nopadtext) {
1388 nopadtext = nopadtext || false;
1390 if (!currentElement) {
1391 return this.domToHTML(this.doc.body);
1394 //Roo.log(currentElement);
1396 var allText = false;
1397 var nodeName = currentElement.nodeName;
1398 var tagName = Roo.util.Format.htmlEncode(currentElement.tagName);
1400 if (nodeName == '#text') {
1402 return nopadtext ? currentElement.nodeValue : currentElement.nodeValue.trim();
1407 if (nodeName != 'BODY') {
1410 // Prints the node tagName, such as <A>, <IMG>, etc
1413 for(i = 0; i < currentElement.attributes.length;i++) {
1415 var aname = currentElement.attributes.item(i).name;
1416 if (!currentElement.attributes.item(i).value.length) {
1419 attr.push(aname + '="' + Roo.util.Format.htmlEncode(currentElement.attributes.item(i).value) + '"' );
1422 ret = "<"+currentElement.tagName+ ( attr.length ? (' ' + attr.join(' ') ) : '') + ">";
1431 if (['IMG', 'BR', 'HR', 'INPUT'].indexOf(tagName) > -1) {
1434 if (['PRE', 'TEXTAREA', 'TD', 'A', 'SPAN'].indexOf(tagName) > -1) { // or code?
1439 // Traverse the tree
1441 var currentElementChild = currentElement.childNodes.item(i);
1445 while (currentElementChild) {
1446 // Formatting code (indent the tree so it looks nice on the screen)
1447 var nopad = nopadtext;
1448 if (lastnode == 'SPAN') {
1452 if (currentElementChild.nodeName == '#text') {
1453 var toadd = Roo.util.Format.htmlEncode(currentElementChild.nodeValue);
1454 toadd = nopadtext ? toadd : toadd.trim();
1455 if (!nopad && toadd.length > 80) {
1456 innerHTML += "\n" + (new Array( depth + 1 )).join( " " );
1461 currentElementChild = currentElement.childNodes.item(i);
1467 innerHTML += nopad ? '' : "\n" + (new Array( depth + 1 )).join( " " );
1469 // Recursively traverse the tree structure of the child node
1470 innerHTML += this.domToHTML(currentElementChild, depth+1, nopadtext);
1471 lastnode = currentElementChild.nodeName;
1473 currentElementChild=currentElement.childNodes.item(i);
1479 // The remaining code is mostly for formatting the tree
1480 ret+= nopadtext ? '' : "\n" + (new Array( depth )).join( " " );
1485 ret+= "</"+tagName+">";
1491 applyBlacklists : function()
1493 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1494 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1498 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1499 if (b.indexOf(tag) > -1) {
1502 this.white.push(tag);
1506 Roo.each(w, function(tag) {
1507 if (b.indexOf(tag) > -1) {
1510 if (this.white.indexOf(tag) > -1) {
1513 this.white.push(tag);
1518 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1519 if (w.indexOf(tag) > -1) {
1522 this.black.push(tag);
1526 Roo.each(b, function(tag) {
1527 if (w.indexOf(tag) > -1) {
1530 if (this.black.indexOf(tag) > -1) {
1533 this.black.push(tag);
1538 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1539 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1543 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1544 if (b.indexOf(tag) > -1) {
1547 this.cwhite.push(tag);
1551 Roo.each(w, function(tag) {
1552 if (b.indexOf(tag) > -1) {
1555 if (this.cwhite.indexOf(tag) > -1) {
1558 this.cwhite.push(tag);
1563 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1564 if (w.indexOf(tag) > -1) {
1567 this.cblack.push(tag);
1571 Roo.each(b, function(tag) {
1572 if (w.indexOf(tag) > -1) {
1575 if (this.cblack.indexOf(tag) > -1) {
1578 this.cblack.push(tag);
1583 setStylesheets : function(stylesheets)
1585 if(typeof(stylesheets) == 'string'){
1586 Roo.get(this.iframe.contentDocument.head).createChild({
1597 Roo.each(stylesheets, function(s) {
1602 Roo.get(_this.iframe.contentDocument.head).createChild({
1613 removeStylesheets : function()
1617 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1622 setStyle : function(style)
1624 Roo.get(this.iframe.contentDocument.head).createChild({
1633 // hide stuff that is not compatible
1651 * @cfg {String} fieldClass @hide
1654 * @cfg {String} focusClass @hide
1657 * @cfg {String} autoCreate @hide
1660 * @cfg {String} inputType @hide
1663 * @cfg {String} invalidClass @hide
1666 * @cfg {String} invalidText @hide
1669 * @cfg {String} msgFx @hide
1672 * @cfg {String} validateOnBlur @hide
1676 Roo.HtmlEditorCore.white = [
1677 'area', 'br', 'img', 'input', 'hr', 'wbr',
1679 'address', 'blockquote', 'center', 'dd', 'dir', 'div',
1680 'dl', 'dt', 'h1', 'h2', 'h3', 'h4',
1681 'h5', 'h6', 'hr', 'isindex', 'listing', 'marquee',
1682 'menu', 'multicol', 'ol', 'p', 'plaintext', 'pre',
1683 'table', 'ul', 'xmp',
1685 'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
1688 'dir', 'menu', 'ol', 'ul', 'dl',
1694 Roo.HtmlEditorCore.black = [
1695 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1697 'base', 'basefont', 'bgsound', 'blink', 'body',
1698 'frame', 'frameset', 'head', 'html', 'ilayer',
1699 'iframe', 'layer', 'link', 'meta', 'object',
1700 'script', 'style' ,'title', 'xml' // clean later..
1702 Roo.HtmlEditorCore.clean = [
1703 'script', 'style', 'title', 'xml'
1705 Roo.HtmlEditorCore.remove = [
1710 Roo.HtmlEditorCore.ablack = [
1714 Roo.HtmlEditorCore.aclean = [
1715 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1719 Roo.HtmlEditorCore.pwhite= [
1720 'http', 'https', 'mailto'
1723 // white listed style attributes.
1724 Roo.HtmlEditorCore.cwhite= [
1725 // 'text-align', /// default is to allow most things..
1731 // black listed style attributes.
1732 Roo.HtmlEditorCore.cblack= [
1733 // 'font-size' -- this can be set by the project
1737 Roo.HtmlEditorCore.swapCodes =[
1738 [ 8211, "–" ],
1739 [ 8212, "—" ],