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.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 + ">');
581 var html = cd.getData('text/html'); // clipboard event
582 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
583 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
587 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
588 .map(function(g) { return g.toDataURL(); });
591 html = this.cleanWordChars(html);
593 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
596 var sn = this.getParentElement();
597 // check if d contains a table, and prevent nesting??
598 //Roo.log(d.getElementsByTagName('table'));
600 //Roo.log(sn.closest('table'));
601 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
603 this.insertAtCursor("You can not nest tables");
604 //Roo.log("prevent?"); // fixme -
608 if (images.length > 0) {
609 Roo.each(d.getElementsByTagName('img'), function(img, i) {
610 img.setAttribute('src', images[i]);
613 if (this.autoClean) {
614 new Roo.htmleditor.FilterStyleToTag({ node : d });
615 new Roo.htmleditor.FilterAttributes({
617 attrib_white : ['href', 'src', 'name', 'align'],
618 attrib_clean : ['href', 'src' ]
620 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
622 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
623 new Roo.htmleditor.FilterParagraph({ node : d });
624 new Roo.htmleditor.FilterSpan({ node : d });
625 new Roo.htmleditor.FilterLongBr({ node : d });
627 if (this.enableBlocks) {
629 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
630 if (img.closest('figure')) { // assume!! that it's aready
633 var fig = new Roo.htmleditor.BlockFigure({
636 fig.updateElement(img); // replace it..
642 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
643 if (this.enableBlocks) {
644 Roo.htmleditor.Block.initAll(this.doc.body);
650 // default behaveiour should be our local cleanup paste? (optional?)
651 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
652 //this.owner.fireEvent('paste', e, v);
655 onDestroy : function(){
661 //for (var i =0; i < this.toolbars.length;i++) {
662 // // fixme - ask toolbars for heights?
663 // this.toolbars[i].onDestroy();
666 //this.wrap.dom.innerHTML = '';
667 //this.wrap.remove();
672 onFirstFocus : function(){
675 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
677 this.activated = true;
680 if(Roo.isGecko){ // prevent silly gecko errors
682 var s = this.win.getSelection();
683 if(!s.focusNode || s.focusNode.nodeType != 3){
684 var r = s.getRangeAt(0);
685 r.selectNodeContents((this.doc.body || this.doc.documentElement));
690 this.execCmd('useCSS', true);
691 this.execCmd('styleWithCSS', false);
694 this.owner.fireEvent('activate', this);
698 adjustFont: function(btn){
699 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
700 //if(Roo.isSafari){ // safari
703 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
704 if(Roo.isSafari){ // safari
705 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
706 v = (v < 10) ? 10 : v;
707 v = (v > 48) ? 48 : v;
708 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
713 v = Math.max(1, v+adjust);
715 this.execCmd('FontSize', v );
718 onEditorEvent : function(e)
722 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
723 return; // we do not handle this.. (undo manager does..)
725 // in theory this detects if the last element is not a br, then we try and do that.
726 // its so clicking in space at bottom triggers adding a br and moving the cursor.
728 e.target.nodeName == 'BODY' &&
729 e.type == "mouseup" &&
730 this.doc.body.lastChild
732 var lc = this.doc.body.lastChild;
733 // gtx-trans is google translate plugin adding crap.
734 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
735 lc = lc.previousSibling;
737 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
738 // if last element is <BR> - then dont do anything.
740 var ns = this.doc.createElement('br');
741 this.doc.body.appendChild(ns);
742 range = this.doc.createRange();
743 range.setStartAfter(ns);
744 range.collapse(true);
745 var sel = this.win.getSelection();
746 sel.removeAllRanges();
753 this.fireEditorEvent(e);
754 // this.updateToolbar();
755 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
758 fireEditorEvent: function(e)
760 this.owner.fireEvent('editorevent', this, e);
763 insertTag : function(tg)
765 // could be a bit smarter... -> wrap the current selected tRoo..
766 if (tg.toLowerCase() == 'span' ||
767 tg.toLowerCase() == 'code' ||
768 tg.toLowerCase() == 'sup' ||
769 tg.toLowerCase() == 'sub'
772 range = this.createRange(this.getSelection());
773 var wrappingNode = this.doc.createElement(tg.toLowerCase());
774 wrappingNode.appendChild(range.extractContents());
775 range.insertNode(wrappingNode);
782 this.execCmd("formatblock", tg);
783 this.undoManager.addEvent();
786 insertText : function(txt)
790 var range = this.createRange();
791 range.deleteContents();
792 //alert(Sender.getAttribute('label'));
794 range.insertNode(this.doc.createTextNode(txt));
795 this.undoManager.addEvent();
801 * Executes a Midas editor command on the editor document and performs necessary focus and
802 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
803 * @param {String} cmd The Midas command
804 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
806 relayCmd : function(cmd, value)
812 case 'justifycenter':
813 // if we are in a cell, then we will adjust the
814 var n = this.getParentElement();
815 var td = n.closest('td');
817 var bl = Roo.htmleditor.Block.factory(td);
818 bl.textAlign = cmd.replace('justify','');
820 this.owner.fireEvent('editorevent', this);
823 this.execCmd('styleWithCSS', true); //
827 // if there is no selection, then we insert, and set the curson inside it..
828 this.execCmd('styleWithCSS', false);
838 this.execCmd(cmd, value);
839 this.owner.fireEvent('editorevent', this);
840 //this.updateToolbar();
841 this.owner.deferFocus();
845 * Executes a Midas editor command directly on the editor document.
846 * For visual commands, you should use {@link #relayCmd} instead.
847 * <b>This should only be called after the editor is initialized.</b>
848 * @param {String} cmd The Midas command
849 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
851 execCmd : function(cmd, value){
852 this.doc.execCommand(cmd, false, value === undefined ? null : value);
859 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
861 * @param {String} text | dom node..
863 insertAtCursor : function(text)
870 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
874 // from jquery ui (MIT licenced)
878 if (win.getSelection && win.getSelection().getRangeAt) {
880 // delete the existing?
882 this.createRange(this.getSelection()).deleteContents();
883 range = win.getSelection().getRangeAt(0);
884 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
885 range.insertNode(node);
886 range = range.cloneRange();
887 range.collapse(false);
889 win.getSelection().removeAllRanges();
890 win.getSelection().addRange(range);
894 } else if (win.document.selection && win.document.selection.createRange) {
895 // no firefox support
896 var txt = typeof(text) == 'string' ? text : text.outerHTML;
897 win.document.selection.createRange().pasteHTML(txt);
900 // no firefox support
901 var txt = typeof(text) == 'string' ? text : text.outerHTML;
902 this.execCmd('InsertHTML', txt);
910 mozKeyPress : function(e){
912 var c = e.getCharCode(), cmd;
915 c = String.fromCharCode(c).toLowerCase();
929 // this.cleanUpPaste.defer(100, this);
947 fixKeys : function(){ // load time branching for fastest keydown performance
952 var k = e.getKey(), r;
955 r = this.doc.selection.createRange();
958 r.pasteHTML('    ');
963 /// this is handled by Roo.htmleditor.KeyEnter
966 r = this.doc.selection.createRange();
968 var target = r.parentElement();
969 if(!target || target.tagName.toLowerCase() != 'li'){
971 r.pasteHTML('<br/>');
978 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
979 // this.cleanUpPaste.defer(100, this);
985 }else if(Roo.isOpera){
991 this.execCmd('InsertHTML','    ');
995 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
996 // this.cleanUpPaste.defer(100, this);
1001 }else if(Roo.isSafari){
1007 this.execCmd('InsertText','\t');
1011 this.mozKeyPress(e);
1013 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1014 // this.cleanUpPaste.defer(100, this);
1022 getAllAncestors: function()
1024 var p = this.getSelectedNode();
1027 a.push(p); // push blank onto stack..
1028 p = this.getParentElement();
1032 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1036 a.push(this.doc.body);
1040 lastSelNode : false,
1043 getSelection : function()
1045 this.assignDocWin();
1046 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1050 * @param {DomElement} node the node to select
1052 selectNode : function(node, collapse)
1054 var nodeRange = node.ownerDocument.createRange();
1056 nodeRange.selectNode(node);
1058 nodeRange.selectNodeContents(node);
1060 if (collapse === true) {
1061 nodeRange.collapse(true);
1064 var s = this.win.getSelection();
1065 s.removeAllRanges();
1066 s.addRange(nodeRange);
1069 getSelectedNode: function()
1071 // this may only work on Gecko!!!
1073 // should we cache this!!!!
1077 var range = this.createRange(this.getSelection()).cloneRange();
1080 var parent = range.parentElement();
1082 var testRange = range.duplicate();
1083 testRange.moveToElementText(parent);
1084 if (testRange.inRange(range)) {
1087 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1090 parent = parent.parentElement;
1095 // is ancestor a text element.
1096 var ac = range.commonAncestorContainer;
1097 if (ac.nodeType == 3) {
1101 var ar = ac.childNodes;
1104 var other_nodes = [];
1105 var has_other_nodes = false;
1106 for (var i=0;i<ar.length;i++) {
1107 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1110 // fullly contained node.
1112 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1117 // probably selected..
1118 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1119 other_nodes.push(ar[i]);
1123 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1128 has_other_nodes = true;
1130 if (!nodes.length && other_nodes.length) {
1133 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1141 createRange: function(sel)
1143 // this has strange effects when using with
1144 // top toolbar - not sure if it's a great idea.
1145 //this.editor.contentWindow.focus();
1146 if (typeof sel != "undefined") {
1148 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1150 return this.doc.createRange();
1153 return this.doc.createRange();
1156 getParentElement: function()
1159 this.assignDocWin();
1160 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1162 var range = this.createRange(sel);
1165 var p = range.commonAncestorContainer;
1166 while (p.nodeType == 3) { // text node
1177 * Range intersection.. the hard stuff...
1181 * [ -- selected range --- ]
1185 * if end is before start or hits it. fail.
1186 * if start is after end or hits it fail.
1188 * if either hits (but other is outside. - then it's not
1194 // @see http://www.thismuchiknow.co.uk/?p=64.
1195 rangeIntersectsNode : function(range, node)
1197 var nodeRange = node.ownerDocument.createRange();
1199 nodeRange.selectNode(node);
1201 nodeRange.selectNodeContents(node);
1204 var rangeStartRange = range.cloneRange();
1205 rangeStartRange.collapse(true);
1207 var rangeEndRange = range.cloneRange();
1208 rangeEndRange.collapse(false);
1210 var nodeStartRange = nodeRange.cloneRange();
1211 nodeStartRange.collapse(true);
1213 var nodeEndRange = nodeRange.cloneRange();
1214 nodeEndRange.collapse(false);
1216 return rangeStartRange.compareBoundaryPoints(
1217 Range.START_TO_START, nodeEndRange) == -1 &&
1218 rangeEndRange.compareBoundaryPoints(
1219 Range.START_TO_START, nodeStartRange) == 1;
1223 rangeCompareNode : function(range, node)
1225 var nodeRange = node.ownerDocument.createRange();
1227 nodeRange.selectNode(node);
1229 nodeRange.selectNodeContents(node);
1233 range.collapse(true);
1235 nodeRange.collapse(true);
1237 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1238 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1240 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1242 var nodeIsBefore = ss == 1;
1243 var nodeIsAfter = ee == -1;
1245 if (nodeIsBefore && nodeIsAfter) {
1248 if (!nodeIsBefore && nodeIsAfter) {
1249 return 1; //right trailed.
1252 if (nodeIsBefore && !nodeIsAfter) {
1253 return 2; // left trailed.
1259 cleanWordChars : function(input) {// change the chars to hex code
1262 [ 8211, "–" ],
1263 [ 8212, "—" ],
1272 Roo.each(swapCodes, function(sw) {
1273 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1275 output = output.replace(swapper, sw[1]);
1285 cleanUpChild : function (node)
1288 new Roo.htmleditor.FilterComment({node : node});
1289 new Roo.htmleditor.FilterAttributes({
1291 attrib_black : this.ablack,
1292 attrib_clean : this.aclean,
1293 style_white : this.cwhite,
1294 style_black : this.cblack
1296 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1297 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1303 * Clean up MS wordisms...
1304 * @deprecated - use filter directly
1306 cleanWord : function(node)
1308 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1315 * @deprecated - use filters
1317 cleanTableWidths : function(node)
1319 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1326 applyBlacklists : function()
1328 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1329 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1331 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1332 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1333 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1337 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1338 if (b.indexOf(tag) > -1) {
1341 this.white.push(tag);
1345 Roo.each(w, function(tag) {
1346 if (b.indexOf(tag) > -1) {
1349 if (this.white.indexOf(tag) > -1) {
1352 this.white.push(tag);
1357 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1358 if (w.indexOf(tag) > -1) {
1361 this.black.push(tag);
1365 Roo.each(b, function(tag) {
1366 if (w.indexOf(tag) > -1) {
1369 if (this.black.indexOf(tag) > -1) {
1372 this.black.push(tag);
1377 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1378 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1382 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1383 if (b.indexOf(tag) > -1) {
1386 this.cwhite.push(tag);
1390 Roo.each(w, function(tag) {
1391 if (b.indexOf(tag) > -1) {
1394 if (this.cwhite.indexOf(tag) > -1) {
1397 this.cwhite.push(tag);
1402 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1403 if (w.indexOf(tag) > -1) {
1406 this.cblack.push(tag);
1410 Roo.each(b, function(tag) {
1411 if (w.indexOf(tag) > -1) {
1414 if (this.cblack.indexOf(tag) > -1) {
1417 this.cblack.push(tag);
1422 setStylesheets : function(stylesheets)
1424 if(typeof(stylesheets) == 'string'){
1425 Roo.get(this.iframe.contentDocument.head).createChild({
1436 Roo.each(stylesheets, function(s) {
1441 Roo.get(_this.iframe.contentDocument.head).createChild({
1453 updateLanguage : function()
1455 if (!this.iframe || !this.iframe.contentDocument) {
1458 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1462 removeStylesheets : function()
1466 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1471 setStyle : function(style)
1473 Roo.get(this.iframe.contentDocument.head).createChild({
1482 // hide stuff that is not compatible
1500 * @cfg {String} fieldClass @hide
1503 * @cfg {String} focusClass @hide
1506 * @cfg {String} autoCreate @hide
1509 * @cfg {String} inputType @hide
1512 * @cfg {String} invalidClass @hide
1515 * @cfg {String} invalidText @hide
1518 * @cfg {String} msgFx @hide
1521 * @cfg {String} validateOnBlur @hide
1525 Roo.HtmlEditorCore.white = [
1526 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1528 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1529 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1530 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1531 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1532 'TABLE', 'UL', 'XMP',
1534 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1537 'DIR', 'MENU', 'OL', 'UL', 'DL',
1543 Roo.HtmlEditorCore.black = [
1544 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1546 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1547 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1548 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1549 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1550 //'FONT' // CLEAN LATER..
1551 'COLGROUP', 'COL' // messy tables.
1555 Roo.HtmlEditorCore.clean = [ // ?? needed???
1556 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1558 Roo.HtmlEditorCore.tag_remove = [
1563 Roo.HtmlEditorCore.ablack = [
1567 Roo.HtmlEditorCore.aclean = [
1568 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1572 Roo.HtmlEditorCore.pwhite= [
1573 'http', 'https', 'mailto'
1576 // white listed style attributes.
1577 Roo.HtmlEditorCore.cwhite= [
1578 // 'text-align', /// default is to allow most things..
1584 // black listed style attributes.
1585 Roo.HtmlEditorCore.cblack= [
1586 // 'font-size' -- this can be set by the project