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
452 Roo.htmleditor.Block.initAll(this.doc.body);
453 this.updateLanguage();
455 var lc = this.doc.body.lastChild;
456 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
457 // add an extra line at the end.
458 this.doc.body.appendChild(this.doc.createElement('br'));
466 deferFocus : function(){
467 this.focus.defer(10, this);
472 if(this.win && !this.sourceEditMode){
479 assignDocWin: function()
481 var iframe = this.iframe;
484 this.doc = iframe.contentWindow.document;
485 this.win = iframe.contentWindow;
487 // if (!Roo.get(this.frameId)) {
490 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
491 // this.win = Roo.get(this.frameId).dom.contentWindow;
493 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
497 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
498 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
503 initEditor : function(){
504 //console.log("INIT EDITOR");
509 this.doc.designMode="on";
511 this.doc.write(this.getDocMarkup());
514 var dbody = (this.doc.body || this.doc.documentElement);
515 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
516 // this copies styles from the containing element into thsi one..
517 // not sure why we need all of this..
518 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
520 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
521 //ss['background-attachment'] = 'fixed'; // w3c
522 dbody.bgProperties = 'fixed'; // ie
523 dbody.setAttribute("translate", "no");
525 //Roo.DomHelper.applyStyles(dbody, ss);
526 Roo.EventManager.on(this.doc, {
528 'mouseup': this.onEditorEvent,
529 'dblclick': this.onEditorEvent,
530 'click': this.onEditorEvent,
531 'keyup': this.onEditorEvent,
536 Roo.EventManager.on(this.doc, {
537 'paste': this.onPasteEvent,
541 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
544 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
545 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
547 this.initialized = true;
550 // initialize special key events - enter
551 new Roo.htmleditor.KeyEnter({core : this});
555 this.owner.fireEvent('initialize', this);
558 // this is to prevent a href clicks resulting in a redirect?
560 onPasteEvent : function(e,v)
562 // I think we better assume paste is going to be a dirty load of rubish from word..
564 // even pasting into a 'email version' of this widget will have to clean up that mess.
565 var cd = (e.browserEvent.clipboardData || window.clipboardData);
567 // check what type of paste - if it's an image, then handle it differently.
568 if (cd.files.length > 0) {
570 var urlAPI = (window.createObjectURL && window) ||
571 (window.URL && URL.revokeObjectURL && URL) ||
572 (window.webkitURL && webkitURL);
574 var url = urlAPI.createObjectURL( cd.files[0]);
575 this.insertAtCursor('<img src=" + url + ">');
579 var html = cd.getData('text/html'); // clipboard event
580 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
581 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
585 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
586 .map(function(g) { return g.toDataURL(); });
589 html = this.cleanWordChars(html);
591 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
594 var sn = this.getParentElement();
595 // check if d contains a table, and prevent nesting??
596 //Roo.log(d.getElementsByTagName('table'));
598 //Roo.log(sn.closest('table'));
599 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
601 this.insertAtCursor("You can not nest tables");
602 //Roo.log("prevent?"); // fixme -
606 if (images.length > 0) {
607 Roo.each(d.getElementsByTagName('img'), function(img, i) {
608 img.setAttribute('src', images[i]);
611 if (this.autoClean) {
612 new Roo.htmleditor.FilterStyleToTag({ node : d });
613 new Roo.htmleditor.FilterAttributes({
615 attrib_white : ['href', 'src', 'name', 'align'],
616 attrib_clean : ['href', 'src' ]
618 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
620 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
621 new Roo.htmleditor.FilterParagraph({ node : d });
622 new Roo.htmleditor.FilterSpan({ node : d });
623 new Roo.htmleditor.FilterLongBr({ node : d });
625 if (this.enableBlocks) {
627 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
628 if (img.closest('figure')) { // assume!! that it's aready
631 var fig = new Roo.htmleditor.BlockFigure({
634 fig.updateElement(img); // replace it..
640 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
641 if (this.enableBlocks) {
642 Roo.htmleditor.Block.initAll(this.doc.body);
648 // default behaveiour should be our local cleanup paste? (optional?)
649 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
650 //this.owner.fireEvent('paste', e, v);
653 onDestroy : function(){
659 //for (var i =0; i < this.toolbars.length;i++) {
660 // // fixme - ask toolbars for heights?
661 // this.toolbars[i].onDestroy();
664 //this.wrap.dom.innerHTML = '';
665 //this.wrap.remove();
670 onFirstFocus : function(){
673 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
675 this.activated = true;
678 if(Roo.isGecko){ // prevent silly gecko errors
680 var s = this.win.getSelection();
681 if(!s.focusNode || s.focusNode.nodeType != 3){
682 var r = s.getRangeAt(0);
683 r.selectNodeContents((this.doc.body || this.doc.documentElement));
688 this.execCmd('useCSS', true);
689 this.execCmd('styleWithCSS', false);
692 this.owner.fireEvent('activate', this);
696 adjustFont: function(btn){
697 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
698 //if(Roo.isSafari){ // safari
701 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
702 if(Roo.isSafari){ // safari
703 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
704 v = (v < 10) ? 10 : v;
705 v = (v > 48) ? 48 : v;
706 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
711 v = Math.max(1, v+adjust);
713 this.execCmd('FontSize', v );
716 onEditorEvent : function(e)
720 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
721 return; // we do not handle this.. (undo manager does..)
723 // in theory this detects if the last element is not a br, then we try and do that.
724 // its so clicking in space at bottom triggers adding a br and moving the cursor.
726 e.target.nodeName == 'BODY' &&
727 e.type == "mouseup" &&
728 this.doc.body.lastChild
730 var lc = this.doc.body.lastChild;
731 // gtx-trans is google translate plugin adding crap.
732 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
733 lc = lc.previousSibling;
735 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
736 // if last element is <BR> - then dont do anything.
738 var ns = this.doc.createElement('br');
739 this.doc.body.appendChild(ns);
740 range = this.doc.createRange();
741 range.setStartAfter(ns);
742 range.collapse(true);
743 var sel = this.win.getSelection();
744 sel.removeAllRanges();
751 this.fireEditorEvent(e);
752 // this.updateToolbar();
753 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
756 fireEditorEvent: function(e)
758 this.owner.fireEvent('editorevent', this, e);
761 insertTag : function(tg)
763 // could be a bit smarter... -> wrap the current selected tRoo..
764 if (tg.toLowerCase() == 'span' ||
765 tg.toLowerCase() == 'code' ||
766 tg.toLowerCase() == 'sup' ||
767 tg.toLowerCase() == 'sub'
770 range = this.createRange(this.getSelection());
771 var wrappingNode = this.doc.createElement(tg.toLowerCase());
772 wrappingNode.appendChild(range.extractContents());
773 range.insertNode(wrappingNode);
780 this.execCmd("formatblock", tg);
781 this.undoManager.addEvent();
784 insertText : function(txt)
788 var range = this.createRange();
789 range.deleteContents();
790 //alert(Sender.getAttribute('label'));
792 range.insertNode(this.doc.createTextNode(txt));
793 this.undoManager.addEvent();
799 * Executes a Midas editor command on the editor document and performs necessary focus and
800 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
801 * @param {String} cmd The Midas command
802 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
804 relayCmd : function(cmd, value)
810 case 'justifycenter':
811 // if we are in a cell, then we will adjust the
812 var n = this.getParentElement();
813 var td = n.closest('td');
815 var bl = Roo.htmleditor.Block.factory(td);
816 bl.textAlign = cmd.replace('justify','');
818 this.owner.fireEvent('editorevent', this);
821 this.execCmd('styleWithCSS', true); //
825 // if there is no selection, then we insert, and set the curson inside it..
826 this.execCmd('styleWithCSS', false);
836 this.execCmd(cmd, value);
837 this.owner.fireEvent('editorevent', this);
838 //this.updateToolbar();
839 this.owner.deferFocus();
843 * Executes a Midas editor command directly on the editor document.
844 * For visual commands, you should use {@link #relayCmd} instead.
845 * <b>This should only be called after the editor is initialized.</b>
846 * @param {String} cmd The Midas command
847 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
849 execCmd : function(cmd, value){
850 this.doc.execCommand(cmd, false, value === undefined ? null : value);
857 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
859 * @param {String} text | dom node..
861 insertAtCursor : function(text)
868 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
872 // from jquery ui (MIT licenced)
876 if (win.getSelection && win.getSelection().getRangeAt) {
878 // delete the existing?
880 this.createRange(this.getSelection()).deleteContents();
881 range = win.getSelection().getRangeAt(0);
882 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
883 range.insertNode(node);
884 range = range.cloneRange();
885 range.collapse(false);
887 win.getSelection().removeAllRanges();
888 win.getSelection().addRange(range);
892 } else if (win.document.selection && win.document.selection.createRange) {
893 // no firefox support
894 var txt = typeof(text) == 'string' ? text : text.outerHTML;
895 win.document.selection.createRange().pasteHTML(txt);
898 // no firefox support
899 var txt = typeof(text) == 'string' ? text : text.outerHTML;
900 this.execCmd('InsertHTML', txt);
908 mozKeyPress : function(e){
910 var c = e.getCharCode(), cmd;
913 c = String.fromCharCode(c).toLowerCase();
927 // this.cleanUpPaste.defer(100, this);
945 fixKeys : function(){ // load time branching for fastest keydown performance
950 var k = e.getKey(), r;
953 r = this.doc.selection.createRange();
956 r.pasteHTML('    ');
961 /// this is handled by Roo.htmleditor.KeyEnter
964 r = this.doc.selection.createRange();
966 var target = r.parentElement();
967 if(!target || target.tagName.toLowerCase() != 'li'){
969 r.pasteHTML('<br/>');
976 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
977 // this.cleanUpPaste.defer(100, this);
983 }else if(Roo.isOpera){
989 this.execCmd('InsertHTML','    ');
993 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
994 // this.cleanUpPaste.defer(100, this);
999 }else if(Roo.isSafari){
1005 this.execCmd('InsertText','\t');
1009 this.mozKeyPress(e);
1011 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1012 // this.cleanUpPaste.defer(100, this);
1020 getAllAncestors: function()
1022 var p = this.getSelectedNode();
1025 a.push(p); // push blank onto stack..
1026 p = this.getParentElement();
1030 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1034 a.push(this.doc.body);
1038 lastSelNode : false,
1041 getSelection : function()
1043 this.assignDocWin();
1044 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1048 * @param {DomElement} node the node to select
1050 selectNode : function(node, collapse)
1052 var nodeRange = node.ownerDocument.createRange();
1054 nodeRange.selectNode(node);
1056 nodeRange.selectNodeContents(node);
1058 if (collapse === true) {
1059 nodeRange.collapse(true);
1062 var s = this.win.getSelection();
1063 s.removeAllRanges();
1064 s.addRange(nodeRange);
1067 getSelectedNode: function()
1069 // this may only work on Gecko!!!
1071 // should we cache this!!!!
1075 var range = this.createRange(this.getSelection()).cloneRange();
1078 var parent = range.parentElement();
1080 var testRange = range.duplicate();
1081 testRange.moveToElementText(parent);
1082 if (testRange.inRange(range)) {
1085 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1088 parent = parent.parentElement;
1093 // is ancestor a text element.
1094 var ac = range.commonAncestorContainer;
1095 if (ac.nodeType == 3) {
1099 var ar = ac.childNodes;
1102 var other_nodes = [];
1103 var has_other_nodes = false;
1104 for (var i=0;i<ar.length;i++) {
1105 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1108 // fullly contained node.
1110 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1115 // probably selected..
1116 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1117 other_nodes.push(ar[i]);
1121 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1126 has_other_nodes = true;
1128 if (!nodes.length && other_nodes.length) {
1131 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1139 createRange: function(sel)
1141 // this has strange effects when using with
1142 // top toolbar - not sure if it's a great idea.
1143 //this.editor.contentWindow.focus();
1144 if (typeof sel != "undefined") {
1146 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1148 return this.doc.createRange();
1151 return this.doc.createRange();
1154 getParentElement: function()
1157 this.assignDocWin();
1158 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1160 var range = this.createRange(sel);
1163 var p = range.commonAncestorContainer;
1164 while (p.nodeType == 3) { // text node
1175 * Range intersection.. the hard stuff...
1179 * [ -- selected range --- ]
1183 * if end is before start or hits it. fail.
1184 * if start is after end or hits it fail.
1186 * if either hits (but other is outside. - then it's not
1192 // @see http://www.thismuchiknow.co.uk/?p=64.
1193 rangeIntersectsNode : function(range, node)
1195 var nodeRange = node.ownerDocument.createRange();
1197 nodeRange.selectNode(node);
1199 nodeRange.selectNodeContents(node);
1202 var rangeStartRange = range.cloneRange();
1203 rangeStartRange.collapse(true);
1205 var rangeEndRange = range.cloneRange();
1206 rangeEndRange.collapse(false);
1208 var nodeStartRange = nodeRange.cloneRange();
1209 nodeStartRange.collapse(true);
1211 var nodeEndRange = nodeRange.cloneRange();
1212 nodeEndRange.collapse(false);
1214 return rangeStartRange.compareBoundaryPoints(
1215 Range.START_TO_START, nodeEndRange) == -1 &&
1216 rangeEndRange.compareBoundaryPoints(
1217 Range.START_TO_START, nodeStartRange) == 1;
1221 rangeCompareNode : function(range, node)
1223 var nodeRange = node.ownerDocument.createRange();
1225 nodeRange.selectNode(node);
1227 nodeRange.selectNodeContents(node);
1231 range.collapse(true);
1233 nodeRange.collapse(true);
1235 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1236 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1238 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1240 var nodeIsBefore = ss == 1;
1241 var nodeIsAfter = ee == -1;
1243 if (nodeIsBefore && nodeIsAfter) {
1246 if (!nodeIsBefore && nodeIsAfter) {
1247 return 1; //right trailed.
1250 if (nodeIsBefore && !nodeIsAfter) {
1251 return 2; // left trailed.
1257 cleanWordChars : function(input) {// change the chars to hex code
1260 [ 8211, "–" ],
1261 [ 8212, "—" ],
1270 Roo.each(swapCodes, function(sw) {
1271 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1273 output = output.replace(swapper, sw[1]);
1283 cleanUpChild : function (node)
1286 new Roo.htmleditor.FilterComment({node : node});
1287 new Roo.htmleditor.FilterAttributes({
1289 attrib_black : this.ablack,
1290 attrib_clean : this.aclean,
1291 style_white : this.cwhite,
1292 style_black : this.cblack
1294 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1295 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1301 * Clean up MS wordisms...
1302 * @deprecated - use filter directly
1304 cleanWord : function(node)
1306 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1313 * @deprecated - use filters
1315 cleanTableWidths : function(node)
1317 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1324 applyBlacklists : function()
1326 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1327 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1329 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1330 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1331 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1335 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1336 if (b.indexOf(tag) > -1) {
1339 this.white.push(tag);
1343 Roo.each(w, function(tag) {
1344 if (b.indexOf(tag) > -1) {
1347 if (this.white.indexOf(tag) > -1) {
1350 this.white.push(tag);
1355 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1356 if (w.indexOf(tag) > -1) {
1359 this.black.push(tag);
1363 Roo.each(b, function(tag) {
1364 if (w.indexOf(tag) > -1) {
1367 if (this.black.indexOf(tag) > -1) {
1370 this.black.push(tag);
1375 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1376 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1380 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1381 if (b.indexOf(tag) > -1) {
1384 this.cwhite.push(tag);
1388 Roo.each(w, function(tag) {
1389 if (b.indexOf(tag) > -1) {
1392 if (this.cwhite.indexOf(tag) > -1) {
1395 this.cwhite.push(tag);
1400 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1401 if (w.indexOf(tag) > -1) {
1404 this.cblack.push(tag);
1408 Roo.each(b, function(tag) {
1409 if (w.indexOf(tag) > -1) {
1412 if (this.cblack.indexOf(tag) > -1) {
1415 this.cblack.push(tag);
1420 setStylesheets : function(stylesheets)
1422 if(typeof(stylesheets) == 'string'){
1423 Roo.get(this.iframe.contentDocument.head).createChild({
1434 Roo.each(stylesheets, function(s) {
1439 Roo.get(_this.iframe.contentDocument.head).createChild({
1451 updateLanguage : function()
1453 if (!this.iframe || !this.iframe.contentDocument) {
1456 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1460 removeStylesheets : function()
1464 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1469 setStyle : function(style)
1471 Roo.get(this.iframe.contentDocument.head).createChild({
1480 // hide stuff that is not compatible
1498 * @cfg {String} fieldClass @hide
1501 * @cfg {String} focusClass @hide
1504 * @cfg {String} autoCreate @hide
1507 * @cfg {String} inputType @hide
1510 * @cfg {String} invalidClass @hide
1513 * @cfg {String} invalidText @hide
1516 * @cfg {String} msgFx @hide
1519 * @cfg {String} validateOnBlur @hide
1523 Roo.HtmlEditorCore.white = [
1524 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1526 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1527 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1528 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1529 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1530 'TABLE', 'UL', 'XMP',
1532 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1535 'DIR', 'MENU', 'OL', 'UL', 'DL',
1541 Roo.HtmlEditorCore.black = [
1542 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1544 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1545 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1546 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1547 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1548 //'FONT' // CLEAN LATER..
1549 'COLGROUP', 'COL' // messy tables.
1553 Roo.HtmlEditorCore.clean = [ // ?? needed???
1554 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1556 Roo.HtmlEditorCore.tag_remove = [
1561 Roo.HtmlEditorCore.ablack = [
1565 Roo.HtmlEditorCore.aclean = [
1566 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1570 Roo.HtmlEditorCore.pwhite= [
1571 'http', 'https', 'mailto'
1574 // white listed style attributes.
1575 Roo.HtmlEditorCore.cwhite= [
1576 // 'text-align', /// default is to allow most things..
1582 // black listed style attributes.
1583 Roo.HtmlEditorCore.cblack= [
1584 // 'font-size' -- this can be set by the project