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
79 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
81 // defaults : white / black...
82 this.applyBlacklists();
89 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
93 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
99 * @cfg {String} resizable 's' or 'se' or 'e' - wrapps the element in a
104 * @cfg {Number} height (in pixels)
108 * @cfg {Number} width (in pixels)
112 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
113 * if you are doing an email editor, this probably needs disabling, it's designed
118 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
122 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
127 * @cfg {String} language default en - language of text (usefull for rtl languages)
133 * @cfg {boolean} allowComments - default false - allow comments in HTML source
134 * - by default they are stripped - if you are editing email you may need this.
136 allowComments: false,
140 // private properties
141 validationEvent : false,
145 sourceEditMode : false,
146 onFocus : Roo.emptyFn,
152 // blacklist + whitelisted elements..
161 * Protected method that will not generally be called directly. It
162 * is called when the editor initializes the iframe with HTML contents. Override this method if you
163 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
165 getDocMarkup : function(){
169 // inherit styels from page...??
170 if (this.stylesheets === false) {
172 Roo.get(document.head).select('style').each(function(node) {
173 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
176 Roo.get(document.head).select('link').each(function(node) {
177 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
180 } else if (!this.stylesheets.length) {
182 st = '<style type="text/css">' +
183 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
186 for (var i in this.stylesheets) {
187 if (typeof(this.stylesheets[i]) != 'string') {
190 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
195 st += '<style type="text/css">' +
196 'IMG { cursor: pointer } ' +
199 st += '<meta name="google" content="notranslate">';
201 var cls = 'notranslate roo-htmleditor-body';
203 if(this.bodyCls.length){
204 cls += ' ' + this.bodyCls;
207 return '<html class="notranslate" translate="no"><head>' + st +
208 //<style type="text/css">' +
209 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
211 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
215 onRender : function(ct, position)
218 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
219 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
222 this.el.dom.style.border = '0 none';
223 this.el.dom.setAttribute('tabIndex', -1);
224 this.el.addClass('x-hidden hide');
228 if(Roo.isIE){ // fix IE 1px bogus margin
229 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
233 this.frameId = Roo.id();
237 var iframe = this.owner.wrap.createChild({
239 cls: 'form-control', // bootstrap..
243 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
248 this.iframe = iframe.dom;
252 this.doc.designMode = 'on';
255 this.doc.write(this.getDocMarkup());
259 var task = { // must defer to wait for browser to be ready
261 //console.log("run task?" + this.doc.readyState);
263 if(this.doc.body || this.doc.readyState == 'complete'){
265 this.doc.designMode="on";
270 Roo.TaskMgr.stop(task);
271 this.initEditor.defer(10, this);
278 Roo.TaskMgr.start(task);
283 onResize : function(w, h)
285 Roo.log('resize: ' +w + ',' + h );
286 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
290 if(typeof w == 'number'){
292 this.iframe.style.width = w + 'px';
294 if(typeof h == 'number'){
296 this.iframe.style.height = h + 'px';
298 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
305 * Toggles the editor between standard and source edit mode.
306 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
308 toggleSourceEdit : function(sourceEditMode){
310 this.sourceEditMode = sourceEditMode === true;
312 if(this.sourceEditMode){
314 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
317 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
318 //this.iframe.className = '';
321 //this.setSize(this.owner.wrap.getSize());
322 //this.fireEvent('editmodechange', this, this.sourceEditMode);
329 * Protected method that will not generally be called directly. If you need/want
330 * custom HTML cleanup, this is the method you should override.
331 * @param {String} html The HTML to be cleaned
332 * return {String} The cleaned HTML
334 cleanHtml : function(html)
338 if(Roo.isSafari){ // strip safari nonsense
339 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
342 if(html == ' '){
349 * HTML Editor -> Textarea
350 * Protected method that will not generally be called directly. Syncs the contents
351 * of the editor iframe with the textarea.
353 syncValue : function()
355 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
356 if(this.initialized){
358 this.undoManager.addEvent();
361 var bd = (this.doc.body || this.doc.documentElement);
364 var sel = this.win.getSelection();
366 var div = document.createElement('div');
367 div.innerHTML = bd.innerHTML;
368 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
369 if (gtx.length > 0) {
370 var rm = gtx.item(0).parentNode;
371 rm.parentNode.removeChild(rm);
375 if (this.enableBlocks) {
376 new Roo.htmleditor.FilterBlock({ node : div });
379 var tidy = new Roo.htmleditor.TidySerializer({
382 var html = tidy.serialize(div);
386 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
387 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
389 html = '<div style="'+m[0]+'">' + html + '</div>';
392 html = this.cleanHtml(html);
393 // fix up the special chars.. normaly like back quotes in word...
394 // however we do not want to do this with chinese..
395 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
397 var cc = match.charCodeAt();
399 // Get the character value, handling surrogate pairs
400 if (match.length == 2) {
401 // It's a surrogate pair, calculate the Unicode code point
402 var high = match.charCodeAt(0) - 0xD800;
403 var low = match.charCodeAt(1) - 0xDC00;
404 cc = (high * 0x400) + low + 0x10000;
406 (cc >= 0x4E00 && cc < 0xA000 ) ||
407 (cc >= 0x3400 && cc < 0x4E00 ) ||
408 (cc >= 0xf900 && cc < 0xfb00 )
413 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
414 return "&#" + cc + ";";
421 if(this.owner.fireEvent('beforesync', this, html) !== false){
422 this.el.dom.value = html;
423 this.owner.fireEvent('sync', this, html);
429 * TEXTAREA -> EDITABLE
430 * Protected method that will not generally be called directly. Pushes the value of the textarea
431 * into the iframe editor.
433 pushValue : function()
435 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
436 if(this.initialized){
437 var v = this.el.dom.value.trim();
440 if(this.owner.fireEvent('beforepush', this, v) !== false){
441 var d = (this.doc.body || this.doc.documentElement);
444 this.el.dom.value = d.innerHTML;
445 this.owner.fireEvent('push', this, v);
447 if (this.autoClean) {
448 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
449 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
451 if (this.enableBlocks) {
452 Roo.htmleditor.Block.initAll(this.doc.body);
455 this.updateLanguage();
457 var lc = this.doc.body.lastChild;
458 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
459 // add an extra line at the end.
460 this.doc.body.appendChild(this.doc.createElement('br'));
468 deferFocus : function(){
469 this.focus.defer(10, this);
474 if(this.win && !this.sourceEditMode){
481 assignDocWin: function()
483 var iframe = this.iframe;
486 this.doc = iframe.contentWindow.document;
487 this.win = iframe.contentWindow;
489 // if (!Roo.get(this.frameId)) {
492 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
493 // this.win = Roo.get(this.frameId).dom.contentWindow;
495 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
499 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
500 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
505 initEditor : function(){
506 //console.log("INIT EDITOR");
511 this.doc.designMode="on";
513 this.doc.write(this.getDocMarkup());
516 var dbody = (this.doc.body || this.doc.documentElement);
517 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
518 // this copies styles from the containing element into thsi one..
519 // not sure why we need all of this..
520 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
522 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
523 //ss['background-attachment'] = 'fixed'; // w3c
524 dbody.bgProperties = 'fixed'; // ie
525 dbody.setAttribute("translate", "no");
527 //Roo.DomHelper.applyStyles(dbody, ss);
528 Roo.EventManager.on(this.doc, {
530 'mouseup': this.onEditorEvent,
531 'dblclick': this.onEditorEvent,
532 'click': this.onEditorEvent,
533 'keyup': this.onEditorEvent,
538 Roo.EventManager.on(this.doc, {
539 'paste': this.onPasteEvent,
543 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
546 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
547 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
549 this.initialized = true;
552 // initialize special key events - enter
553 new Roo.htmleditor.KeyEnter({core : this});
557 this.owner.fireEvent('initialize', this);
560 // this is to prevent a href clicks resulting in a redirect?
562 onPasteEvent : function(e,v)
564 // I think we better assume paste is going to be a dirty load of rubish from word..
566 // even pasting into a 'email version' of this widget will have to clean up that mess.
567 var cd = (e.browserEvent.clipboardData || window.clipboardData);
569 // check what type of paste - if it's an image, then handle it differently.
570 if (cd.files && cd.files.length > 0) {
572 var urlAPI = (window.createObjectURL && window) ||
573 (window.URL && URL.revokeObjectURL && URL) ||
574 (window.webkitURL && webkitURL);
576 var url = urlAPI.createObjectURL( cd.files[0]);
577 this.insertAtCursor('<img src=" + url + ">');
580 if (cd.types.indexOf('text/html') < 0 ) {
584 var html = cd.getData('text/html'); // clipboard event
585 if (cd.types.indexOf('text/rtf') > -1) {
586 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
587 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
592 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
593 .map(function(g) { return g.toDataURL(); })
594 .filter(function(g) { return g != 'about:blank'; });
597 html = this.cleanWordChars(html);
599 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
602 var sn = this.getParentElement();
603 // check if d contains a table, and prevent nesting??
604 //Roo.log(d.getElementsByTagName('table'));
606 //Roo.log(sn.closest('table'));
607 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
609 this.insertAtCursor("You can not nest tables");
610 //Roo.log("prevent?"); // fixme -
614 if (images.length > 0) {
615 Roo.each(d.getElementsByTagName('img'), function(img, i) {
616 img.setAttribute('src', images[i]);
619 if (this.autoClean) {
620 new Roo.htmleditor.FilterStyleToTag({ node : d });
621 new Roo.htmleditor.FilterAttributes({
623 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display'],
624 attrib_clean : ['href', 'src' ]
626 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
628 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
629 new Roo.htmleditor.FilterParagraph({ node : d });
630 new Roo.htmleditor.FilterSpan({ node : d });
631 new Roo.htmleditor.FilterLongBr({ node : d });
632 new Roo.htmleditor.FilterComment({ node : d });
634 if (this.enableBlocks) {
636 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
637 if (img.closest('figure')) { // assume!! that it's aready
640 var fig = new Roo.htmleditor.BlockFigure({
643 fig.updateElement(img); // replace it..
649 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
650 if (this.enableBlocks) {
651 Roo.htmleditor.Block.initAll(this.doc.body);
657 // default behaveiour should be our local cleanup paste? (optional?)
658 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
659 //this.owner.fireEvent('paste', e, v);
662 onDestroy : function(){
668 //for (var i =0; i < this.toolbars.length;i++) {
669 // // fixme - ask toolbars for heights?
670 // this.toolbars[i].onDestroy();
673 //this.wrap.dom.innerHTML = '';
674 //this.wrap.remove();
679 onFirstFocus : function(){
682 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
684 this.activated = true;
687 if(Roo.isGecko){ // prevent silly gecko errors
689 var s = this.win.getSelection();
690 if(!s.focusNode || s.focusNode.nodeType != 3){
691 var r = s.getRangeAt(0);
692 r.selectNodeContents((this.doc.body || this.doc.documentElement));
697 this.execCmd('useCSS', true);
698 this.execCmd('styleWithCSS', false);
701 this.owner.fireEvent('activate', this);
705 adjustFont: function(btn){
706 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
707 //if(Roo.isSafari){ // safari
710 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
711 if(Roo.isSafari){ // safari
712 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
713 v = (v < 10) ? 10 : v;
714 v = (v > 48) ? 48 : v;
715 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
720 v = Math.max(1, v+adjust);
722 this.execCmd('FontSize', v );
725 onEditorEvent : function(e)
729 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
730 return; // we do not handle this.. (undo manager does..)
732 // in theory this detects if the last element is not a br, then we try and do that.
733 // its so clicking in space at bottom triggers adding a br and moving the cursor.
735 e.target.nodeName == 'BODY' &&
736 e.type == "mouseup" &&
737 this.doc.body.lastChild
739 var lc = this.doc.body.lastChild;
740 // gtx-trans is google translate plugin adding crap.
741 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
742 lc = lc.previousSibling;
744 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
745 // if last element is <BR> - then dont do anything.
747 var ns = this.doc.createElement('br');
748 this.doc.body.appendChild(ns);
749 range = this.doc.createRange();
750 range.setStartAfter(ns);
751 range.collapse(true);
752 var sel = this.win.getSelection();
753 sel.removeAllRanges();
760 this.fireEditorEvent(e);
761 // this.updateToolbar();
762 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
765 fireEditorEvent: function(e)
767 this.owner.fireEvent('editorevent', this, e);
770 insertTag : function(tg)
772 // could be a bit smarter... -> wrap the current selected tRoo..
773 if (tg.toLowerCase() == 'span' ||
774 tg.toLowerCase() == 'code' ||
775 tg.toLowerCase() == 'sup' ||
776 tg.toLowerCase() == 'sub'
779 range = this.createRange(this.getSelection());
780 var wrappingNode = this.doc.createElement(tg.toLowerCase());
781 wrappingNode.appendChild(range.extractContents());
782 range.insertNode(wrappingNode);
789 this.execCmd("formatblock", tg);
790 this.undoManager.addEvent();
793 insertText : function(txt)
797 var range = this.createRange();
798 range.deleteContents();
799 //alert(Sender.getAttribute('label'));
801 range.insertNode(this.doc.createTextNode(txt));
802 this.undoManager.addEvent();
808 * Executes a Midas editor command on the editor document and performs necessary focus and
809 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
810 * @param {String} cmd The Midas command
811 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
813 relayCmd : function(cmd, value)
819 case 'justifycenter':
820 // if we are in a cell, then we will adjust the
821 var n = this.getParentElement();
822 var td = n.closest('td');
824 var bl = Roo.htmleditor.Block.factory(td);
825 bl.textAlign = cmd.replace('justify','');
827 this.owner.fireEvent('editorevent', this);
830 this.execCmd('styleWithCSS', true); //
834 // if there is no selection, then we insert, and set the curson inside it..
835 this.execCmd('styleWithCSS', false);
845 this.execCmd(cmd, value);
846 this.owner.fireEvent('editorevent', this);
847 //this.updateToolbar();
848 this.owner.deferFocus();
852 * Executes a Midas editor command directly on the editor document.
853 * For visual commands, you should use {@link #relayCmd} instead.
854 * <b>This should only be called after the editor is initialized.</b>
855 * @param {String} cmd The Midas command
856 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
858 execCmd : function(cmd, value){
859 this.doc.execCommand(cmd, false, value === undefined ? null : value);
866 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
868 * @param {String} text | dom node..
870 insertAtCursor : function(text)
877 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
881 // from jquery ui (MIT licenced)
885 if (win.getSelection && win.getSelection().getRangeAt) {
887 // delete the existing?
889 this.createRange(this.getSelection()).deleteContents();
890 range = win.getSelection().getRangeAt(0);
891 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
892 range.insertNode(node);
893 range = range.cloneRange();
894 range.collapse(false);
896 win.getSelection().removeAllRanges();
897 win.getSelection().addRange(range);
901 } else if (win.document.selection && win.document.selection.createRange) {
902 // no firefox support
903 var txt = typeof(text) == 'string' ? text : text.outerHTML;
904 win.document.selection.createRange().pasteHTML(txt);
907 // no firefox support
908 var txt = typeof(text) == 'string' ? text : text.outerHTML;
909 this.execCmd('InsertHTML', txt);
917 mozKeyPress : function(e){
919 var c = e.getCharCode(), cmd;
922 c = String.fromCharCode(c).toLowerCase();
936 // this.cleanUpPaste.defer(100, this);
954 fixKeys : function(){ // load time branching for fastest keydown performance
959 var k = e.getKey(), r;
962 r = this.doc.selection.createRange();
965 r.pasteHTML('    ');
970 /// this is handled by Roo.htmleditor.KeyEnter
973 r = this.doc.selection.createRange();
975 var target = r.parentElement();
976 if(!target || target.tagName.toLowerCase() != 'li'){
978 r.pasteHTML('<br/>');
985 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
986 // this.cleanUpPaste.defer(100, this);
992 }else if(Roo.isOpera){
998 this.execCmd('InsertHTML','    ');
1002 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1003 // this.cleanUpPaste.defer(100, this);
1008 }else if(Roo.isSafari){
1014 this.execCmd('InsertText','\t');
1018 this.mozKeyPress(e);
1020 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1021 // this.cleanUpPaste.defer(100, this);
1029 getAllAncestors: function()
1031 var p = this.getSelectedNode();
1034 a.push(p); // push blank onto stack..
1035 p = this.getParentElement();
1039 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1043 a.push(this.doc.body);
1047 lastSelNode : false,
1050 getSelection : function()
1052 this.assignDocWin();
1053 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1057 * @param {DomElement} node the node to select
1059 selectNode : function(node, collapse)
1061 var nodeRange = node.ownerDocument.createRange();
1063 nodeRange.selectNode(node);
1065 nodeRange.selectNodeContents(node);
1067 if (collapse === true) {
1068 nodeRange.collapse(true);
1071 var s = this.win.getSelection();
1072 s.removeAllRanges();
1073 s.addRange(nodeRange);
1076 getSelectedNode: function()
1078 // this may only work on Gecko!!!
1080 // should we cache this!!!!
1084 var range = this.createRange(this.getSelection()).cloneRange();
1087 var parent = range.parentElement();
1089 var testRange = range.duplicate();
1090 testRange.moveToElementText(parent);
1091 if (testRange.inRange(range)) {
1094 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1097 parent = parent.parentElement;
1102 // is ancestor a text element.
1103 var ac = range.commonAncestorContainer;
1104 if (ac.nodeType == 3) {
1108 var ar = ac.childNodes;
1111 var other_nodes = [];
1112 var has_other_nodes = false;
1113 for (var i=0;i<ar.length;i++) {
1114 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1117 // fullly contained node.
1119 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1124 // probably selected..
1125 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1126 other_nodes.push(ar[i]);
1130 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1135 has_other_nodes = true;
1137 if (!nodes.length && other_nodes.length) {
1140 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1148 createRange: function(sel)
1150 // this has strange effects when using with
1151 // top toolbar - not sure if it's a great idea.
1152 //this.editor.contentWindow.focus();
1153 if (typeof sel != "undefined") {
1155 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1157 return this.doc.createRange();
1160 return this.doc.createRange();
1163 getParentElement: function()
1166 this.assignDocWin();
1167 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1169 var range = this.createRange(sel);
1172 var p = range.commonAncestorContainer;
1173 while (p.nodeType == 3) { // text node
1184 * Range intersection.. the hard stuff...
1188 * [ -- selected range --- ]
1192 * if end is before start or hits it. fail.
1193 * if start is after end or hits it fail.
1195 * if either hits (but other is outside. - then it's not
1201 // @see http://www.thismuchiknow.co.uk/?p=64.
1202 rangeIntersectsNode : function(range, node)
1204 var nodeRange = node.ownerDocument.createRange();
1206 nodeRange.selectNode(node);
1208 nodeRange.selectNodeContents(node);
1211 var rangeStartRange = range.cloneRange();
1212 rangeStartRange.collapse(true);
1214 var rangeEndRange = range.cloneRange();
1215 rangeEndRange.collapse(false);
1217 var nodeStartRange = nodeRange.cloneRange();
1218 nodeStartRange.collapse(true);
1220 var nodeEndRange = nodeRange.cloneRange();
1221 nodeEndRange.collapse(false);
1223 return rangeStartRange.compareBoundaryPoints(
1224 Range.START_TO_START, nodeEndRange) == -1 &&
1225 rangeEndRange.compareBoundaryPoints(
1226 Range.START_TO_START, nodeStartRange) == 1;
1230 rangeCompareNode : function(range, node)
1232 var nodeRange = node.ownerDocument.createRange();
1234 nodeRange.selectNode(node);
1236 nodeRange.selectNodeContents(node);
1240 range.collapse(true);
1242 nodeRange.collapse(true);
1244 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1245 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1247 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1249 var nodeIsBefore = ss == 1;
1250 var nodeIsAfter = ee == -1;
1252 if (nodeIsBefore && nodeIsAfter) {
1255 if (!nodeIsBefore && nodeIsAfter) {
1256 return 1; //right trailed.
1259 if (nodeIsBefore && !nodeIsAfter) {
1260 return 2; // left trailed.
1266 cleanWordChars : function(input) {// change the chars to hex code
1269 [ 8211, "–" ],
1270 [ 8212, "—" ],
1279 Roo.each(swapCodes, function(sw) {
1280 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1282 output = output.replace(swapper, sw[1]);
1292 cleanUpChild : function (node)
1295 new Roo.htmleditor.FilterComment({node : node});
1296 new Roo.htmleditor.FilterAttributes({
1298 attrib_black : this.ablack,
1299 attrib_clean : this.aclean,
1300 style_white : this.cwhite,
1301 style_black : this.cblack
1303 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1304 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1310 * Clean up MS wordisms...
1311 * @deprecated - use filter directly
1313 cleanWord : function(node)
1315 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1322 * @deprecated - use filters
1324 cleanTableWidths : function(node)
1326 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1333 applyBlacklists : function()
1335 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1336 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1338 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1339 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1340 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1344 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1345 if (b.indexOf(tag) > -1) {
1348 this.white.push(tag);
1352 Roo.each(w, function(tag) {
1353 if (b.indexOf(tag) > -1) {
1356 if (this.white.indexOf(tag) > -1) {
1359 this.white.push(tag);
1364 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1365 if (w.indexOf(tag) > -1) {
1368 this.black.push(tag);
1372 Roo.each(b, function(tag) {
1373 if (w.indexOf(tag) > -1) {
1376 if (this.black.indexOf(tag) > -1) {
1379 this.black.push(tag);
1384 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1385 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1389 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1390 if (b.indexOf(tag) > -1) {
1393 this.cwhite.push(tag);
1397 Roo.each(w, function(tag) {
1398 if (b.indexOf(tag) > -1) {
1401 if (this.cwhite.indexOf(tag) > -1) {
1404 this.cwhite.push(tag);
1409 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1410 if (w.indexOf(tag) > -1) {
1413 this.cblack.push(tag);
1417 Roo.each(b, function(tag) {
1418 if (w.indexOf(tag) > -1) {
1421 if (this.cblack.indexOf(tag) > -1) {
1424 this.cblack.push(tag);
1429 setStylesheets : function(stylesheets)
1431 if(typeof(stylesheets) == 'string'){
1432 Roo.get(this.iframe.contentDocument.head).createChild({
1443 Roo.each(stylesheets, function(s) {
1448 Roo.get(_this.iframe.contentDocument.head).createChild({
1460 updateLanguage : function()
1462 if (!this.iframe || !this.iframe.contentDocument) {
1465 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1469 removeStylesheets : function()
1473 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1478 setStyle : function(style)
1480 Roo.get(this.iframe.contentDocument.head).createChild({
1489 // hide stuff that is not compatible
1507 * @cfg {String} fieldClass @hide
1510 * @cfg {String} focusClass @hide
1513 * @cfg {String} autoCreate @hide
1516 * @cfg {String} inputType @hide
1519 * @cfg {String} invalidClass @hide
1522 * @cfg {String} invalidText @hide
1525 * @cfg {String} msgFx @hide
1528 * @cfg {String} validateOnBlur @hide
1532 Roo.HtmlEditorCore.white = [
1533 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1535 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1536 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1537 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1538 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1539 'TABLE', 'UL', 'XMP',
1541 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1544 'DIR', 'MENU', 'OL', 'UL', 'DL',
1550 Roo.HtmlEditorCore.black = [
1551 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1553 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1554 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1555 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1556 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1557 //'FONT' // CLEAN LATER..
1558 'COLGROUP', 'COL' // messy tables.
1562 Roo.HtmlEditorCore.clean = [ // ?? needed???
1563 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1565 Roo.HtmlEditorCore.tag_remove = [
1570 Roo.HtmlEditorCore.ablack = [
1574 Roo.HtmlEditorCore.aclean = [
1575 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1579 Roo.HtmlEditorCore.pwhite= [
1580 'http', 'https', 'mailto'
1583 // white listed style attributes.
1584 Roo.HtmlEditorCore.cwhite= [
1585 // 'text-align', /// default is to allow most things..
1591 // black listed style attributes.
1592 Roo.HtmlEditorCore.cblack= [
1593 // 'font-size' -- this can be set by the project