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|footerf)/); }) // ignore headers/footers etc.
588 .map(function(g) { return g.toDataURL(); })
589 .filter(function(g) { return g != 'about:blank'; });
592 html = this.cleanWordChars(html);
594 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
597 var sn = this.getParentElement();
598 // check if d contains a table, and prevent nesting??
599 //Roo.log(d.getElementsByTagName('table'));
601 //Roo.log(sn.closest('table'));
602 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
604 this.insertAtCursor("You can not nest tables");
605 //Roo.log("prevent?"); // fixme -
609 if (images.length > 0) {
610 Roo.each(d.getElementsByTagName('img'), function(img, i) {
611 img.setAttribute('src', images[i]);
614 if (this.autoClean) {
615 new Roo.htmleditor.FilterStyleToTag({ node : d });
616 new Roo.htmleditor.FilterAttributes({
618 attrib_white : ['href', 'src', 'name', 'align'],
619 attrib_clean : ['href', 'src' ]
621 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
623 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
624 new Roo.htmleditor.FilterParagraph({ node : d });
625 new Roo.htmleditor.FilterSpan({ node : d });
626 new Roo.htmleditor.FilterLongBr({ node : d });
628 if (this.enableBlocks) {
630 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
631 if (img.closest('figure')) { // assume!! that it's aready
634 var fig = new Roo.htmleditor.BlockFigure({
637 fig.updateElement(img); // replace it..
643 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
644 if (this.enableBlocks) {
645 Roo.htmleditor.Block.initAll(this.doc.body);
651 // default behaveiour should be our local cleanup paste? (optional?)
652 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
653 //this.owner.fireEvent('paste', e, v);
656 onDestroy : function(){
662 //for (var i =0; i < this.toolbars.length;i++) {
663 // // fixme - ask toolbars for heights?
664 // this.toolbars[i].onDestroy();
667 //this.wrap.dom.innerHTML = '';
668 //this.wrap.remove();
673 onFirstFocus : function(){
676 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
678 this.activated = true;
681 if(Roo.isGecko){ // prevent silly gecko errors
683 var s = this.win.getSelection();
684 if(!s.focusNode || s.focusNode.nodeType != 3){
685 var r = s.getRangeAt(0);
686 r.selectNodeContents((this.doc.body || this.doc.documentElement));
691 this.execCmd('useCSS', true);
692 this.execCmd('styleWithCSS', false);
695 this.owner.fireEvent('activate', this);
699 adjustFont: function(btn){
700 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
701 //if(Roo.isSafari){ // safari
704 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
705 if(Roo.isSafari){ // safari
706 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
707 v = (v < 10) ? 10 : v;
708 v = (v > 48) ? 48 : v;
709 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
714 v = Math.max(1, v+adjust);
716 this.execCmd('FontSize', v );
719 onEditorEvent : function(e)
723 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
724 return; // we do not handle this.. (undo manager does..)
726 // in theory this detects if the last element is not a br, then we try and do that.
727 // its so clicking in space at bottom triggers adding a br and moving the cursor.
729 e.target.nodeName == 'BODY' &&
730 e.type == "mouseup" &&
731 this.doc.body.lastChild
733 var lc = this.doc.body.lastChild;
734 // gtx-trans is google translate plugin adding crap.
735 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
736 lc = lc.previousSibling;
738 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
739 // if last element is <BR> - then dont do anything.
741 var ns = this.doc.createElement('br');
742 this.doc.body.appendChild(ns);
743 range = this.doc.createRange();
744 range.setStartAfter(ns);
745 range.collapse(true);
746 var sel = this.win.getSelection();
747 sel.removeAllRanges();
754 this.fireEditorEvent(e);
755 // this.updateToolbar();
756 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
759 fireEditorEvent: function(e)
761 this.owner.fireEvent('editorevent', this, e);
764 insertTag : function(tg)
766 // could be a bit smarter... -> wrap the current selected tRoo..
767 if (tg.toLowerCase() == 'span' ||
768 tg.toLowerCase() == 'code' ||
769 tg.toLowerCase() == 'sup' ||
770 tg.toLowerCase() == 'sub'
773 range = this.createRange(this.getSelection());
774 var wrappingNode = this.doc.createElement(tg.toLowerCase());
775 wrappingNode.appendChild(range.extractContents());
776 range.insertNode(wrappingNode);
783 this.execCmd("formatblock", tg);
784 this.undoManager.addEvent();
787 insertText : function(txt)
791 var range = this.createRange();
792 range.deleteContents();
793 //alert(Sender.getAttribute('label'));
795 range.insertNode(this.doc.createTextNode(txt));
796 this.undoManager.addEvent();
802 * Executes a Midas editor command on the editor document and performs necessary focus and
803 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
804 * @param {String} cmd The Midas command
805 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
807 relayCmd : function(cmd, value)
813 case 'justifycenter':
814 // if we are in a cell, then we will adjust the
815 var n = this.getParentElement();
816 var td = n.closest('td');
818 var bl = Roo.htmleditor.Block.factory(td);
819 bl.textAlign = cmd.replace('justify','');
821 this.owner.fireEvent('editorevent', this);
824 this.execCmd('styleWithCSS', true); //
828 // if there is no selection, then we insert, and set the curson inside it..
829 this.execCmd('styleWithCSS', false);
839 this.execCmd(cmd, value);
840 this.owner.fireEvent('editorevent', this);
841 //this.updateToolbar();
842 this.owner.deferFocus();
846 * Executes a Midas editor command directly on the editor document.
847 * For visual commands, you should use {@link #relayCmd} instead.
848 * <b>This should only be called after the editor is initialized.</b>
849 * @param {String} cmd The Midas command
850 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
852 execCmd : function(cmd, value){
853 this.doc.execCommand(cmd, false, value === undefined ? null : value);
860 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
862 * @param {String} text | dom node..
864 insertAtCursor : function(text)
871 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
875 // from jquery ui (MIT licenced)
879 if (win.getSelection && win.getSelection().getRangeAt) {
881 // delete the existing?
883 this.createRange(this.getSelection()).deleteContents();
884 range = win.getSelection().getRangeAt(0);
885 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
886 range.insertNode(node);
887 range = range.cloneRange();
888 range.collapse(false);
890 win.getSelection().removeAllRanges();
891 win.getSelection().addRange(range);
895 } else if (win.document.selection && win.document.selection.createRange) {
896 // no firefox support
897 var txt = typeof(text) == 'string' ? text : text.outerHTML;
898 win.document.selection.createRange().pasteHTML(txt);
901 // no firefox support
902 var txt = typeof(text) == 'string' ? text : text.outerHTML;
903 this.execCmd('InsertHTML', txt);
911 mozKeyPress : function(e){
913 var c = e.getCharCode(), cmd;
916 c = String.fromCharCode(c).toLowerCase();
930 // this.cleanUpPaste.defer(100, this);
948 fixKeys : function(){ // load time branching for fastest keydown performance
953 var k = e.getKey(), r;
956 r = this.doc.selection.createRange();
959 r.pasteHTML('    ');
964 /// this is handled by Roo.htmleditor.KeyEnter
967 r = this.doc.selection.createRange();
969 var target = r.parentElement();
970 if(!target || target.tagName.toLowerCase() != 'li'){
972 r.pasteHTML('<br/>');
979 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
980 // this.cleanUpPaste.defer(100, this);
986 }else if(Roo.isOpera){
992 this.execCmd('InsertHTML','    ');
996 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
997 // this.cleanUpPaste.defer(100, this);
1002 }else if(Roo.isSafari){
1008 this.execCmd('InsertText','\t');
1012 this.mozKeyPress(e);
1014 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1015 // this.cleanUpPaste.defer(100, this);
1023 getAllAncestors: function()
1025 var p = this.getSelectedNode();
1028 a.push(p); // push blank onto stack..
1029 p = this.getParentElement();
1033 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1037 a.push(this.doc.body);
1041 lastSelNode : false,
1044 getSelection : function()
1046 this.assignDocWin();
1047 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1051 * @param {DomElement} node the node to select
1053 selectNode : function(node, collapse)
1055 var nodeRange = node.ownerDocument.createRange();
1057 nodeRange.selectNode(node);
1059 nodeRange.selectNodeContents(node);
1061 if (collapse === true) {
1062 nodeRange.collapse(true);
1065 var s = this.win.getSelection();
1066 s.removeAllRanges();
1067 s.addRange(nodeRange);
1070 getSelectedNode: function()
1072 // this may only work on Gecko!!!
1074 // should we cache this!!!!
1078 var range = this.createRange(this.getSelection()).cloneRange();
1081 var parent = range.parentElement();
1083 var testRange = range.duplicate();
1084 testRange.moveToElementText(parent);
1085 if (testRange.inRange(range)) {
1088 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1091 parent = parent.parentElement;
1096 // is ancestor a text element.
1097 var ac = range.commonAncestorContainer;
1098 if (ac.nodeType == 3) {
1102 var ar = ac.childNodes;
1105 var other_nodes = [];
1106 var has_other_nodes = false;
1107 for (var i=0;i<ar.length;i++) {
1108 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1111 // fullly contained node.
1113 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1118 // probably selected..
1119 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1120 other_nodes.push(ar[i]);
1124 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1129 has_other_nodes = true;
1131 if (!nodes.length && other_nodes.length) {
1134 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1142 createRange: function(sel)
1144 // this has strange effects when using with
1145 // top toolbar - not sure if it's a great idea.
1146 //this.editor.contentWindow.focus();
1147 if (typeof sel != "undefined") {
1149 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1151 return this.doc.createRange();
1154 return this.doc.createRange();
1157 getParentElement: function()
1160 this.assignDocWin();
1161 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1163 var range = this.createRange(sel);
1166 var p = range.commonAncestorContainer;
1167 while (p.nodeType == 3) { // text node
1178 * Range intersection.. the hard stuff...
1182 * [ -- selected range --- ]
1186 * if end is before start or hits it. fail.
1187 * if start is after end or hits it fail.
1189 * if either hits (but other is outside. - then it's not
1195 // @see http://www.thismuchiknow.co.uk/?p=64.
1196 rangeIntersectsNode : function(range, node)
1198 var nodeRange = node.ownerDocument.createRange();
1200 nodeRange.selectNode(node);
1202 nodeRange.selectNodeContents(node);
1205 var rangeStartRange = range.cloneRange();
1206 rangeStartRange.collapse(true);
1208 var rangeEndRange = range.cloneRange();
1209 rangeEndRange.collapse(false);
1211 var nodeStartRange = nodeRange.cloneRange();
1212 nodeStartRange.collapse(true);
1214 var nodeEndRange = nodeRange.cloneRange();
1215 nodeEndRange.collapse(false);
1217 return rangeStartRange.compareBoundaryPoints(
1218 Range.START_TO_START, nodeEndRange) == -1 &&
1219 rangeEndRange.compareBoundaryPoints(
1220 Range.START_TO_START, nodeStartRange) == 1;
1224 rangeCompareNode : function(range, node)
1226 var nodeRange = node.ownerDocument.createRange();
1228 nodeRange.selectNode(node);
1230 nodeRange.selectNodeContents(node);
1234 range.collapse(true);
1236 nodeRange.collapse(true);
1238 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1239 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1241 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1243 var nodeIsBefore = ss == 1;
1244 var nodeIsAfter = ee == -1;
1246 if (nodeIsBefore && nodeIsAfter) {
1249 if (!nodeIsBefore && nodeIsAfter) {
1250 return 1; //right trailed.
1253 if (nodeIsBefore && !nodeIsAfter) {
1254 return 2; // left trailed.
1260 cleanWordChars : function(input) {// change the chars to hex code
1263 [ 8211, "–" ],
1264 [ 8212, "—" ],
1273 Roo.each(swapCodes, function(sw) {
1274 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1276 output = output.replace(swapper, sw[1]);
1286 cleanUpChild : function (node)
1289 new Roo.htmleditor.FilterComment({node : node});
1290 new Roo.htmleditor.FilterAttributes({
1292 attrib_black : this.ablack,
1293 attrib_clean : this.aclean,
1294 style_white : this.cwhite,
1295 style_black : this.cblack
1297 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1298 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1304 * Clean up MS wordisms...
1305 * @deprecated - use filter directly
1307 cleanWord : function(node)
1309 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1316 * @deprecated - use filters
1318 cleanTableWidths : function(node)
1320 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1327 applyBlacklists : function()
1329 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1330 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1332 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1333 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1334 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1338 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1339 if (b.indexOf(tag) > -1) {
1342 this.white.push(tag);
1346 Roo.each(w, function(tag) {
1347 if (b.indexOf(tag) > -1) {
1350 if (this.white.indexOf(tag) > -1) {
1353 this.white.push(tag);
1358 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1359 if (w.indexOf(tag) > -1) {
1362 this.black.push(tag);
1366 Roo.each(b, function(tag) {
1367 if (w.indexOf(tag) > -1) {
1370 if (this.black.indexOf(tag) > -1) {
1373 this.black.push(tag);
1378 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1379 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1383 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1384 if (b.indexOf(tag) > -1) {
1387 this.cwhite.push(tag);
1391 Roo.each(w, function(tag) {
1392 if (b.indexOf(tag) > -1) {
1395 if (this.cwhite.indexOf(tag) > -1) {
1398 this.cwhite.push(tag);
1403 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1404 if (w.indexOf(tag) > -1) {
1407 this.cblack.push(tag);
1411 Roo.each(b, function(tag) {
1412 if (w.indexOf(tag) > -1) {
1415 if (this.cblack.indexOf(tag) > -1) {
1418 this.cblack.push(tag);
1423 setStylesheets : function(stylesheets)
1425 if(typeof(stylesheets) == 'string'){
1426 Roo.get(this.iframe.contentDocument.head).createChild({
1437 Roo.each(stylesheets, function(s) {
1442 Roo.get(_this.iframe.contentDocument.head).createChild({
1454 updateLanguage : function()
1456 if (!this.iframe || !this.iframe.contentDocument) {
1459 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1463 removeStylesheets : function()
1467 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1472 setStyle : function(style)
1474 Roo.get(this.iframe.contentDocument.head).createChild({
1483 // hide stuff that is not compatible
1501 * @cfg {String} fieldClass @hide
1504 * @cfg {String} focusClass @hide
1507 * @cfg {String} autoCreate @hide
1510 * @cfg {String} inputType @hide
1513 * @cfg {String} invalidClass @hide
1516 * @cfg {String} invalidText @hide
1519 * @cfg {String} msgFx @hide
1522 * @cfg {String} validateOnBlur @hide
1526 Roo.HtmlEditorCore.white = [
1527 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1529 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1530 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1531 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1532 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1533 'TABLE', 'UL', 'XMP',
1535 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1538 'DIR', 'MENU', 'OL', 'UL', 'DL',
1544 Roo.HtmlEditorCore.black = [
1545 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1547 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1548 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1549 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1550 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1551 //'FONT' // CLEAN LATER..
1552 'COLGROUP', 'COL' // messy tables.
1556 Roo.HtmlEditorCore.clean = [ // ?? needed???
1557 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1559 Roo.HtmlEditorCore.tag_remove = [
1564 Roo.HtmlEditorCore.ablack = [
1568 Roo.HtmlEditorCore.aclean = [
1569 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1573 Roo.HtmlEditorCore.pwhite= [
1574 'http', 'https', 'mailto'
1577 // white listed style attributes.
1578 Roo.HtmlEditorCore.cwhite= [
1579 // 'text-align', /// default is to allow most things..
1585 // black listed style attributes.
1586 Roo.HtmlEditorCore.cblack= [
1587 // 'font-size' -- this can be set by the project