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 if (this.undoManager) {
359 this.undoManager.addEvent();
363 var bd = (this.doc.body || this.doc.documentElement);
366 var sel = this.win.getSelection();
368 var div = document.createElement('div');
369 div.innerHTML = bd.innerHTML;
370 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
371 if (gtx.length > 0) {
372 var rm = gtx.item(0).parentNode;
373 rm.parentNode.removeChild(rm);
377 if (this.enableBlocks) {
378 new Roo.htmleditor.FilterBlock({ node : div });
381 var tidy = new Roo.htmleditor.TidySerializer({
384 var html = tidy.serialize(div);
388 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
389 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
391 html = '<div style="'+m[0]+'">' + html + '</div>';
394 html = this.cleanHtml(html);
395 // fix up the special chars.. normaly like back quotes in word...
396 // however we do not want to do this with chinese..
397 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
399 var cc = match.charCodeAt();
401 // Get the character value, handling surrogate pairs
402 if (match.length == 2) {
403 // It's a surrogate pair, calculate the Unicode code point
404 var high = match.charCodeAt(0) - 0xD800;
405 var low = match.charCodeAt(1) - 0xDC00;
406 cc = (high * 0x400) + low + 0x10000;
408 (cc >= 0x4E00 && cc < 0xA000 ) ||
409 (cc >= 0x3400 && cc < 0x4E00 ) ||
410 (cc >= 0xf900 && cc < 0xfb00 )
415 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
416 return "&#" + cc + ";";
423 if(this.owner.fireEvent('beforesync', this, html) !== false){
424 this.el.dom.value = html;
425 this.owner.fireEvent('sync', this, html);
431 * TEXTAREA -> EDITABLE
432 * Protected method that will not generally be called directly. Pushes the value of the textarea
433 * into the iframe editor.
435 pushValue : function()
437 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
438 if(this.initialized){
439 var v = this.el.dom.value.trim();
442 if(this.owner.fireEvent('beforepush', this, v) !== false){
443 var d = (this.doc.body || this.doc.documentElement);
446 this.el.dom.value = d.innerHTML;
447 this.owner.fireEvent('push', this, v);
449 if (this.autoClean) {
450 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
451 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
453 if (this.enableBlocks) {
454 Roo.htmleditor.Block.initAll(this.doc.body);
457 this.updateLanguage();
459 var lc = this.doc.body.lastChild;
460 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
461 // add an extra line at the end.
462 this.doc.body.appendChild(this.doc.createElement('br'));
470 deferFocus : function(){
471 this.focus.defer(10, this);
476 if(this.win && !this.sourceEditMode){
483 assignDocWin: function()
485 var iframe = this.iframe;
488 this.doc = iframe.contentWindow.document;
489 this.win = iframe.contentWindow;
491 // if (!Roo.get(this.frameId)) {
494 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
495 // this.win = Roo.get(this.frameId).dom.contentWindow;
497 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
501 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
502 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
507 initEditor : function(){
508 //console.log("INIT EDITOR");
513 this.doc.designMode="on";
515 this.doc.write(this.getDocMarkup());
518 var dbody = (this.doc.body || this.doc.documentElement);
519 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
520 // this copies styles from the containing element into thsi one..
521 // not sure why we need all of this..
522 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
524 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
525 //ss['background-attachment'] = 'fixed'; // w3c
526 dbody.bgProperties = 'fixed'; // ie
527 dbody.setAttribute("translate", "no");
529 //Roo.DomHelper.applyStyles(dbody, ss);
530 Roo.EventManager.on(this.doc, {
532 'mouseup': this.onEditorEvent,
533 'dblclick': this.onEditorEvent,
534 'click': this.onEditorEvent,
535 'keyup': this.onEditorEvent,
540 Roo.EventManager.on(this.doc, {
541 'paste': this.onPasteEvent,
545 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
548 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
549 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
551 this.initialized = true;
554 // initialize special key events - enter
555 new Roo.htmleditor.KeyEnter({core : this});
559 this.owner.fireEvent('initialize', this);
562 // this is to prevent a href clicks resulting in a redirect?
564 onPasteEvent : function(e,v)
566 // I think we better assume paste is going to be a dirty load of rubish from word..
568 // even pasting into a 'email version' of this widget will have to clean up that mess.
569 var cd = (e.browserEvent.clipboardData || window.clipboardData);
571 // check what type of paste - if it's an image, then handle it differently.
572 if (cd.files && cd.files.length > 0) {
574 var urlAPI = (window.createObjectURL && window) ||
575 (window.URL && URL.revokeObjectURL && URL) ||
576 (window.webkitURL && webkitURL);
578 var url = urlAPI.createObjectURL( cd.files[0]);
579 this.insertAtCursor('<img src=" + url + ">');
582 if (cd.types.indexOf('text/html') < 0 ) {
586 var html = cd.getData('text/html'); // clipboard event
587 if (cd.types.indexOf('text/rtf') > -1) {
588 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
589 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
594 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
595 .map(function(g) { return g.toDataURL(); })
596 .filter(function(g) { return g != 'about:blank'; });
599 html = this.cleanWordChars(html);
601 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
604 var sn = this.getParentElement();
605 // check if d contains a table, and prevent nesting??
606 //Roo.log(d.getElementsByTagName('table'));
608 //Roo.log(sn.closest('table'));
609 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
611 this.insertAtCursor("You can not nest tables");
612 //Roo.log("prevent?"); // fixme -
616 if (images.length > 0) {
617 Roo.each(d.getElementsByTagName('img'), function(img, i) {
618 img.setAttribute('src', images[i]);
621 if (this.autoClean) {
622 new Roo.htmleditor.FilterWord({ node : d });
624 new Roo.htmleditor.FilterStyleToTag({ node : d });
625 new Roo.htmleditor.FilterAttributes({
627 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width'],
628 attrib_clean : ['href', 'src' ]
630 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
632 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
633 new Roo.htmleditor.FilterParagraph({ node : d });
634 new Roo.htmleditor.FilterSpan({ node : d });
635 new Roo.htmleditor.FilterLongBr({ node : d });
636 new Roo.htmleditor.FilterComment({ node : d });
640 if (this.enableBlocks) {
642 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
643 if (img.closest('figure')) { // assume!! that it's aready
646 var fig = new Roo.htmleditor.BlockFigure({
649 fig.updateElement(img); // replace it..
655 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
656 if (this.enableBlocks) {
657 Roo.htmleditor.Block.initAll(this.doc.body);
663 // default behaveiour should be our local cleanup paste? (optional?)
664 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
665 //this.owner.fireEvent('paste', e, v);
668 onDestroy : function(){
674 //for (var i =0; i < this.toolbars.length;i++) {
675 // // fixme - ask toolbars for heights?
676 // this.toolbars[i].onDestroy();
679 //this.wrap.dom.innerHTML = '';
680 //this.wrap.remove();
685 onFirstFocus : function(){
688 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
690 this.activated = true;
693 if(Roo.isGecko){ // prevent silly gecko errors
695 var s = this.win.getSelection();
696 if(!s.focusNode || s.focusNode.nodeType != 3){
697 var r = s.getRangeAt(0);
698 r.selectNodeContents((this.doc.body || this.doc.documentElement));
703 this.execCmd('useCSS', true);
704 this.execCmd('styleWithCSS', false);
707 this.owner.fireEvent('activate', this);
711 adjustFont: function(btn){
712 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
713 //if(Roo.isSafari){ // safari
716 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
717 if(Roo.isSafari){ // safari
718 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
719 v = (v < 10) ? 10 : v;
720 v = (v > 48) ? 48 : v;
721 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
726 v = Math.max(1, v+adjust);
728 this.execCmd('FontSize', v );
731 onEditorEvent : function(e)
735 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
736 return; // we do not handle this.. (undo manager does..)
738 // in theory this detects if the last element is not a br, then we try and do that.
739 // its so clicking in space at bottom triggers adding a br and moving the cursor.
741 e.target.nodeName == 'BODY' &&
742 e.type == "mouseup" &&
743 this.doc.body.lastChild
745 var lc = this.doc.body.lastChild;
746 // gtx-trans is google translate plugin adding crap.
747 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
748 lc = lc.previousSibling;
750 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
751 // if last element is <BR> - then dont do anything.
753 var ns = this.doc.createElement('br');
754 this.doc.body.appendChild(ns);
755 range = this.doc.createRange();
756 range.setStartAfter(ns);
757 range.collapse(true);
758 var sel = this.win.getSelection();
759 sel.removeAllRanges();
766 this.fireEditorEvent(e);
767 // this.updateToolbar();
768 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
771 fireEditorEvent: function(e)
773 this.owner.fireEvent('editorevent', this, e);
776 insertTag : function(tg)
778 // could be a bit smarter... -> wrap the current selected tRoo..
779 if (tg.toLowerCase() == 'span' ||
780 tg.toLowerCase() == 'code' ||
781 tg.toLowerCase() == 'sup' ||
782 tg.toLowerCase() == 'sub'
785 range = this.createRange(this.getSelection());
786 var wrappingNode = this.doc.createElement(tg.toLowerCase());
787 wrappingNode.appendChild(range.extractContents());
788 range.insertNode(wrappingNode);
795 this.execCmd("formatblock", tg);
796 this.undoManager.addEvent();
799 insertText : function(txt)
803 var range = this.createRange();
804 range.deleteContents();
805 //alert(Sender.getAttribute('label'));
807 range.insertNode(this.doc.createTextNode(txt));
808 this.undoManager.addEvent();
814 * Executes a Midas editor command on the editor document and performs necessary focus and
815 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
816 * @param {String} cmd The Midas command
817 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
819 relayCmd : function(cmd, value)
825 case 'justifycenter':
826 // if we are in a cell, then we will adjust the
827 var n = this.getParentElement();
828 var td = n.closest('td');
830 var bl = Roo.htmleditor.Block.factory(td);
831 bl.textAlign = cmd.replace('justify','');
833 this.owner.fireEvent('editorevent', this);
836 this.execCmd('styleWithCSS', true); //
840 // if there is no selection, then we insert, and set the curson inside it..
841 this.execCmd('styleWithCSS', false);
851 this.execCmd(cmd, value);
852 this.owner.fireEvent('editorevent', this);
853 //this.updateToolbar();
854 this.owner.deferFocus();
858 * Executes a Midas editor command directly on the editor document.
859 * For visual commands, you should use {@link #relayCmd} instead.
860 * <b>This should only be called after the editor is initialized.</b>
861 * @param {String} cmd The Midas command
862 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
864 execCmd : function(cmd, value){
865 this.doc.execCommand(cmd, false, value === undefined ? null : value);
872 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
874 * @param {String} text | dom node..
876 insertAtCursor : function(text)
883 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
887 // from jquery ui (MIT licenced)
891 if (win.getSelection && win.getSelection().getRangeAt) {
893 // delete the existing?
895 this.createRange(this.getSelection()).deleteContents();
896 range = win.getSelection().getRangeAt(0);
897 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
898 range.insertNode(node);
899 range = range.cloneRange();
900 range.collapse(false);
902 win.getSelection().removeAllRanges();
903 win.getSelection().addRange(range);
907 } else if (win.document.selection && win.document.selection.createRange) {
908 // no firefox support
909 var txt = typeof(text) == 'string' ? text : text.outerHTML;
910 win.document.selection.createRange().pasteHTML(txt);
913 // no firefox support
914 var txt = typeof(text) == 'string' ? text : text.outerHTML;
915 this.execCmd('InsertHTML', txt);
923 mozKeyPress : function(e){
925 var c = e.getCharCode(), cmd;
928 c = String.fromCharCode(c).toLowerCase();
942 // this.cleanUpPaste.defer(100, this);
960 fixKeys : function(){ // load time branching for fastest keydown performance
965 var k = e.getKey(), r;
968 r = this.doc.selection.createRange();
971 r.pasteHTML('    ');
976 /// this is handled by Roo.htmleditor.KeyEnter
979 r = this.doc.selection.createRange();
981 var target = r.parentElement();
982 if(!target || target.tagName.toLowerCase() != 'li'){
984 r.pasteHTML('<br/>');
991 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
992 // this.cleanUpPaste.defer(100, this);
998 }else if(Roo.isOpera){
1004 this.execCmd('InsertHTML','    ');
1008 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1009 // this.cleanUpPaste.defer(100, this);
1014 }else if(Roo.isSafari){
1020 this.execCmd('InsertText','\t');
1024 this.mozKeyPress(e);
1026 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1027 // this.cleanUpPaste.defer(100, this);
1035 getAllAncestors: function()
1037 var p = this.getSelectedNode();
1040 a.push(p); // push blank onto stack..
1041 p = this.getParentElement();
1045 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1049 a.push(this.doc.body);
1053 lastSelNode : false,
1056 getSelection : function()
1058 this.assignDocWin();
1059 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1063 * @param {DomElement} node the node to select
1065 selectNode : function(node, collapse)
1067 var nodeRange = node.ownerDocument.createRange();
1069 nodeRange.selectNode(node);
1071 nodeRange.selectNodeContents(node);
1073 if (collapse === true) {
1074 nodeRange.collapse(true);
1077 var s = this.win.getSelection();
1078 s.removeAllRanges();
1079 s.addRange(nodeRange);
1082 getSelectedNode: function()
1084 // this may only work on Gecko!!!
1086 // should we cache this!!!!
1090 var range = this.createRange(this.getSelection()).cloneRange();
1093 var parent = range.parentElement();
1095 var testRange = range.duplicate();
1096 testRange.moveToElementText(parent);
1097 if (testRange.inRange(range)) {
1100 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1103 parent = parent.parentElement;
1108 // is ancestor a text element.
1109 var ac = range.commonAncestorContainer;
1110 if (ac.nodeType == 3) {
1114 var ar = ac.childNodes;
1117 var other_nodes = [];
1118 var has_other_nodes = false;
1119 for (var i=0;i<ar.length;i++) {
1120 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1123 // fullly contained node.
1125 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1130 // probably selected..
1131 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1132 other_nodes.push(ar[i]);
1136 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1141 has_other_nodes = true;
1143 if (!nodes.length && other_nodes.length) {
1146 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1154 createRange: function(sel)
1156 // this has strange effects when using with
1157 // top toolbar - not sure if it's a great idea.
1158 //this.editor.contentWindow.focus();
1159 if (typeof sel != "undefined") {
1161 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1163 return this.doc.createRange();
1166 return this.doc.createRange();
1169 getParentElement: function()
1172 this.assignDocWin();
1173 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1175 var range = this.createRange(sel);
1178 var p = range.commonAncestorContainer;
1179 while (p.nodeType == 3) { // text node
1190 * Range intersection.. the hard stuff...
1194 * [ -- selected range --- ]
1198 * if end is before start or hits it. fail.
1199 * if start is after end or hits it fail.
1201 * if either hits (but other is outside. - then it's not
1207 // @see http://www.thismuchiknow.co.uk/?p=64.
1208 rangeIntersectsNode : function(range, node)
1210 var nodeRange = node.ownerDocument.createRange();
1212 nodeRange.selectNode(node);
1214 nodeRange.selectNodeContents(node);
1217 var rangeStartRange = range.cloneRange();
1218 rangeStartRange.collapse(true);
1220 var rangeEndRange = range.cloneRange();
1221 rangeEndRange.collapse(false);
1223 var nodeStartRange = nodeRange.cloneRange();
1224 nodeStartRange.collapse(true);
1226 var nodeEndRange = nodeRange.cloneRange();
1227 nodeEndRange.collapse(false);
1229 return rangeStartRange.compareBoundaryPoints(
1230 Range.START_TO_START, nodeEndRange) == -1 &&
1231 rangeEndRange.compareBoundaryPoints(
1232 Range.START_TO_START, nodeStartRange) == 1;
1236 rangeCompareNode : function(range, node)
1238 var nodeRange = node.ownerDocument.createRange();
1240 nodeRange.selectNode(node);
1242 nodeRange.selectNodeContents(node);
1246 range.collapse(true);
1248 nodeRange.collapse(true);
1250 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1251 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1253 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1255 var nodeIsBefore = ss == 1;
1256 var nodeIsAfter = ee == -1;
1258 if (nodeIsBefore && nodeIsAfter) {
1261 if (!nodeIsBefore && nodeIsAfter) {
1262 return 1; //right trailed.
1265 if (nodeIsBefore && !nodeIsAfter) {
1266 return 2; // left trailed.
1272 cleanWordChars : function(input) {// change the chars to hex code
1275 [ 8211, "–" ],
1276 [ 8212, "—" ],
1285 Roo.each(swapCodes, function(sw) {
1286 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1288 output = output.replace(swapper, sw[1]);
1298 cleanUpChild : function (node)
1301 new Roo.htmleditor.FilterComment({node : node});
1302 new Roo.htmleditor.FilterAttributes({
1304 attrib_black : this.ablack,
1305 attrib_clean : this.aclean,
1306 style_white : this.cwhite,
1307 style_black : this.cblack
1309 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1310 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1316 * Clean up MS wordisms...
1317 * @deprecated - use filter directly
1319 cleanWord : function(node)
1321 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1328 * @deprecated - use filters
1330 cleanTableWidths : function(node)
1332 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1339 applyBlacklists : function()
1341 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1342 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1344 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1345 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1346 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1350 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1351 if (b.indexOf(tag) > -1) {
1354 this.white.push(tag);
1358 Roo.each(w, function(tag) {
1359 if (b.indexOf(tag) > -1) {
1362 if (this.white.indexOf(tag) > -1) {
1365 this.white.push(tag);
1370 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1371 if (w.indexOf(tag) > -1) {
1374 this.black.push(tag);
1378 Roo.each(b, function(tag) {
1379 if (w.indexOf(tag) > -1) {
1382 if (this.black.indexOf(tag) > -1) {
1385 this.black.push(tag);
1390 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1391 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1395 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1396 if (b.indexOf(tag) > -1) {
1399 this.cwhite.push(tag);
1403 Roo.each(w, function(tag) {
1404 if (b.indexOf(tag) > -1) {
1407 if (this.cwhite.indexOf(tag) > -1) {
1410 this.cwhite.push(tag);
1415 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1416 if (w.indexOf(tag) > -1) {
1419 this.cblack.push(tag);
1423 Roo.each(b, function(tag) {
1424 if (w.indexOf(tag) > -1) {
1427 if (this.cblack.indexOf(tag) > -1) {
1430 this.cblack.push(tag);
1435 setStylesheets : function(stylesheets)
1437 if(typeof(stylesheets) == 'string'){
1438 Roo.get(this.iframe.contentDocument.head).createChild({
1449 Roo.each(stylesheets, function(s) {
1454 Roo.get(_this.iframe.contentDocument.head).createChild({
1466 updateLanguage : function()
1468 if (!this.iframe || !this.iframe.contentDocument) {
1471 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1475 removeStylesheets : function()
1479 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1484 setStyle : function(style)
1486 Roo.get(this.iframe.contentDocument.head).createChild({
1495 // hide stuff that is not compatible
1513 * @cfg {String} fieldClass @hide
1516 * @cfg {String} focusClass @hide
1519 * @cfg {String} autoCreate @hide
1522 * @cfg {String} inputType @hide
1525 * @cfg {String} invalidClass @hide
1528 * @cfg {String} invalidText @hide
1531 * @cfg {String} msgFx @hide
1534 * @cfg {String} validateOnBlur @hide
1538 Roo.HtmlEditorCore.white = [
1539 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1541 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1542 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1543 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1544 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1545 'TABLE', 'UL', 'XMP',
1547 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1550 'DIR', 'MENU', 'OL', 'UL', 'DL',
1556 Roo.HtmlEditorCore.black = [
1557 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1559 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1560 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1561 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1562 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1563 //'FONT' // CLEAN LATER..
1564 'COLGROUP', 'COL' // messy tables.
1568 Roo.HtmlEditorCore.clean = [ // ?? needed???
1569 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1571 Roo.HtmlEditorCore.tag_remove = [
1576 Roo.HtmlEditorCore.ablack = [
1580 Roo.HtmlEditorCore.aclean = [
1581 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1585 Roo.HtmlEditorCore.pwhite= [
1586 'http', 'https', 'mailto'
1589 // white listed style attributes.
1590 Roo.HtmlEditorCore.cwhite= [
1591 // 'text-align', /// default is to allow most things..
1597 // black listed style attributes.
1598 Roo.HtmlEditorCore.cblack= [
1599 // 'font-size' -- this can be set by the project