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} css styling for resizing. (used on bootstrap only)
103 * @cfg {Number} height (in pixels)
107 * @cfg {Number} width (in pixels)
111 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
112 * if you are doing an email editor, this probably needs disabling, it's designed
117 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
121 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
126 * @cfg {String} language default en - language of text (usefull for rtl languages)
132 * @cfg {boolean} allowComments - default false - allow comments in HTML source
133 * - by default they are stripped - if you are editing email you may need this.
135 allowComments: false,
139 // private properties
140 validationEvent : false,
144 sourceEditMode : false,
145 onFocus : Roo.emptyFn,
151 // blacklist + whitelisted elements..
160 * Protected method that will not generally be called directly. It
161 * is called when the editor initializes the iframe with HTML contents. Override this method if you
162 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
164 getDocMarkup : function(){
168 // inherit styels from page...??
169 if (this.stylesheets === false) {
171 Roo.get(document.head).select('style').each(function(node) {
172 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
175 Roo.get(document.head).select('link').each(function(node) {
176 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
179 } else if (!this.stylesheets.length) {
181 st = '<style type="text/css">' +
182 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
185 for (var i in this.stylesheets) {
186 if (typeof(this.stylesheets[i]) != 'string') {
189 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
194 st += '<style type="text/css">' +
195 'IMG { cursor: pointer } ' +
198 st += '<meta name="google" content="notranslate">';
200 var cls = 'notranslate roo-htmleditor-body';
202 if(this.bodyCls.length){
203 cls += ' ' + this.bodyCls;
206 return '<html class="notranslate" translate="no"><head>' + st +
207 //<style type="text/css">' +
208 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
210 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
214 onRender : function(ct, position)
217 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
218 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
221 this.el.dom.style.border = '0 none';
222 this.el.dom.setAttribute('tabIndex', -1);
223 this.el.addClass('x-hidden hide');
227 if(Roo.isIE){ // fix IE 1px bogus margin
228 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
232 this.frameId = Roo.id();
236 cls: 'form-control', // bootstrap..
240 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
243 ifcfg.style = { resize : this.resize };
246 var iframe = this.owner.wrap.createChild(ifcfg, this.el);
249 this.iframe = iframe.dom;
253 this.doc.designMode = 'on';
256 this.doc.write(this.getDocMarkup());
260 var task = { // must defer to wait for browser to be ready
262 //console.log("run task?" + this.doc.readyState);
264 if(this.doc.body || this.doc.readyState == 'complete'){
266 this.doc.designMode="on";
271 Roo.TaskMgr.stop(task);
272 this.initEditor.defer(10, this);
279 Roo.TaskMgr.start(task);
284 onResize : function(w, h)
286 Roo.log('resize: ' +w + ',' + h );
287 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
291 if(typeof w == 'number'){
293 this.iframe.style.width = w + 'px';
295 if(typeof h == 'number'){
297 this.iframe.style.height = h + 'px';
299 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
306 * Toggles the editor between standard and source edit mode.
307 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
309 toggleSourceEdit : function(sourceEditMode){
311 this.sourceEditMode = sourceEditMode === true;
313 if(this.sourceEditMode){
315 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
318 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
319 //this.iframe.className = '';
322 //this.setSize(this.owner.wrap.getSize());
323 //this.fireEvent('editmodechange', this, this.sourceEditMode);
330 * Protected method that will not generally be called directly. If you need/want
331 * custom HTML cleanup, this is the method you should override.
332 * @param {String} html The HTML to be cleaned
333 * return {String} The cleaned HTML
335 cleanHtml : function(html)
339 if(Roo.isSafari){ // strip safari nonsense
340 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
343 if(html == ' '){
350 * HTML Editor -> Textarea
351 * Protected method that will not generally be called directly. Syncs the contents
352 * of the editor iframe with the textarea.
354 syncValue : function()
356 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
357 if(this.initialized){
359 if (this.undoManager) {
360 this.undoManager.addEvent();
364 var bd = (this.doc.body || this.doc.documentElement);
367 var sel = this.win.getSelection();
369 var div = document.createElement('div');
370 div.innerHTML = bd.innerHTML;
371 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
372 if (gtx.length > 0) {
373 var rm = gtx.item(0).parentNode;
374 rm.parentNode.removeChild(rm);
378 if (this.enableBlocks) {
379 new Roo.htmleditor.FilterBlock({ node : div });
382 var html = div.innerHTML;
385 if (this.autoClean) {
387 new Roo.htmleditor.FilterAttributes({
408 attrib_clean : ['href', 'src' ]
411 var tidy = new Roo.htmleditor.TidySerializer({
414 html = tidy.serialize(div);
420 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
421 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
423 html = '<div style="'+m[0]+'">' + html + '</div>';
426 html = this.cleanHtml(html);
427 // fix up the special chars.. normaly like back quotes in word...
428 // however we do not want to do this with chinese..
429 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
431 var cc = match.charCodeAt();
433 // Get the character value, handling surrogate pairs
434 if (match.length == 2) {
435 // It's a surrogate pair, calculate the Unicode code point
436 var high = match.charCodeAt(0) - 0xD800;
437 var low = match.charCodeAt(1) - 0xDC00;
438 cc = (high * 0x400) + low + 0x10000;
440 (cc >= 0x4E00 && cc < 0xA000 ) ||
441 (cc >= 0x3400 && cc < 0x4E00 ) ||
442 (cc >= 0xf900 && cc < 0xfb00 )
447 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
448 return "&#" + cc + ";";
455 if(this.owner.fireEvent('beforesync', this, html) !== false){
456 this.el.dom.value = html;
457 this.owner.fireEvent('sync', this, html);
463 * TEXTAREA -> EDITABLE
464 * Protected method that will not generally be called directly. Pushes the value of the textarea
465 * into the iframe editor.
467 pushValue : function()
469 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
470 if(this.initialized){
471 var v = this.el.dom.value.trim();
474 if(this.owner.fireEvent('beforepush', this, v) !== false){
475 var d = (this.doc.body || this.doc.documentElement);
478 this.el.dom.value = d.innerHTML;
479 this.owner.fireEvent('push', this, v);
481 if (this.autoClean) {
482 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
483 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
485 if (this.enableBlocks) {
486 Roo.htmleditor.Block.initAll(this.doc.body);
489 this.updateLanguage();
491 var lc = this.doc.body.lastChild;
492 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
493 // add an extra line at the end.
494 this.doc.body.appendChild(this.doc.createElement('br'));
502 deferFocus : function(){
503 this.focus.defer(10, this);
508 if(this.win && !this.sourceEditMode){
515 assignDocWin: function()
517 var iframe = this.iframe;
520 this.doc = iframe.contentWindow.document;
521 this.win = iframe.contentWindow;
523 // if (!Roo.get(this.frameId)) {
526 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
527 // this.win = Roo.get(this.frameId).dom.contentWindow;
529 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
533 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
534 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
539 initEditor : function(){
540 //console.log("INIT EDITOR");
545 this.doc.designMode="on";
547 this.doc.write(this.getDocMarkup());
550 var dbody = (this.doc.body || this.doc.documentElement);
551 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
552 // this copies styles from the containing element into thsi one..
553 // not sure why we need all of this..
554 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
556 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
557 //ss['background-attachment'] = 'fixed'; // w3c
558 dbody.bgProperties = 'fixed'; // ie
559 dbody.setAttribute("translate", "no");
561 //Roo.DomHelper.applyStyles(dbody, ss);
562 Roo.EventManager.on(this.doc, {
564 'mouseup': this.onEditorEvent,
565 'dblclick': this.onEditorEvent,
566 'click': this.onEditorEvent,
567 'keyup': this.onEditorEvent,
572 Roo.EventManager.on(this.doc, {
573 'paste': this.onPasteEvent,
577 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
580 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
581 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
583 this.initialized = true;
586 // initialize special key events - enter
587 new Roo.htmleditor.KeyEnter({core : this});
591 this.owner.fireEvent('initialize', this);
594 // this is to prevent a href clicks resulting in a redirect?
596 onPasteEvent : function(e,v)
598 // I think we better assume paste is going to be a dirty load of rubish from word..
600 // even pasting into a 'email version' of this widget will have to clean up that mess.
601 var cd = (e.browserEvent.clipboardData || window.clipboardData);
603 // check what type of paste - if it's an image, then handle it differently.
604 if (cd.files && cd.files.length > 0) {
606 var urlAPI = (window.createObjectURL && window) ||
607 (window.URL && URL.revokeObjectURL && URL) ||
608 (window.webkitURL && webkitURL);
610 var r = new FileReader();
612 r.addEventListener('load',function()
615 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
617 if (t.enableBlocks) {
619 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
620 if (img.closest('figure')) { // assume!! that it's aready
623 var fig = new Roo.htmleditor.BlockFigure({
626 fig.updateElement(img); // replace it..
630 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
631 t.owner.fireEvent('paste', this);
633 r.readAsDataURL(cd.files[0]);
639 if (cd.types.indexOf('text/html') < 0 ) {
643 var html = cd.getData('text/html'); // clipboard event
644 if (cd.types.indexOf('text/rtf') > -1) {
645 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
646 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
651 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
652 .map(function(g) { return g.toDataURL(); })
653 .filter(function(g) { return g != 'about:blank'; });
656 html = this.cleanWordChars(html);
658 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
661 var sn = this.getParentElement();
662 // check if d contains a table, and prevent nesting??
663 //Roo.log(d.getElementsByTagName('table'));
665 //Roo.log(sn.closest('table'));
666 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
668 this.insertAtCursor("You can not nest tables");
669 //Roo.log("prevent?"); // fixme -
675 if (images.length > 0) {
676 // replace all v:imagedata - with img.
677 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
678 Roo.each(ar, function(node) {
679 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
680 node.parentNode.removeChild(node);
684 Roo.each(d.getElementsByTagName('img'), function(img, i) {
685 img.setAttribute('src', images[i]);
688 if (this.autoClean) {
689 new Roo.htmleditor.FilterWord({ node : d });
691 new Roo.htmleditor.FilterStyleToTag({ node : d });
692 new Roo.htmleditor.FilterAttributes({
694 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
695 attrib_clean : ['href', 'src' ]
697 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
699 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
700 new Roo.htmleditor.FilterParagraph({ node : d });
701 new Roo.htmleditor.FilterSpan({ node : d });
702 new Roo.htmleditor.FilterLongBr({ node : d });
703 new Roo.htmleditor.FilterComment({ node : d });
707 if (this.enableBlocks) {
709 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
710 if (img.closest('figure')) { // assume!! that it's aready
713 var fig = new Roo.htmleditor.BlockFigure({
716 fig.updateElement(img); // replace it..
722 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
723 if (this.enableBlocks) {
724 Roo.htmleditor.Block.initAll(this.doc.body);
729 this.owner.fireEvent('paste', this);
731 // default behaveiour should be our local cleanup paste? (optional?)
732 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
733 //this.owner.fireEvent('paste', e, v);
736 onDestroy : function(){
742 //for (var i =0; i < this.toolbars.length;i++) {
743 // // fixme - ask toolbars for heights?
744 // this.toolbars[i].onDestroy();
747 //this.wrap.dom.innerHTML = '';
748 //this.wrap.remove();
753 onFirstFocus : function(){
756 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
758 this.activated = true;
761 if(Roo.isGecko){ // prevent silly gecko errors
763 var s = this.win.getSelection();
764 if(!s.focusNode || s.focusNode.nodeType != 3){
765 var r = s.getRangeAt(0);
766 r.selectNodeContents((this.doc.body || this.doc.documentElement));
771 this.execCmd('useCSS', true);
772 this.execCmd('styleWithCSS', false);
775 this.owner.fireEvent('activate', this);
779 adjustFont: function(btn){
780 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
781 //if(Roo.isSafari){ // safari
784 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
785 if(Roo.isSafari){ // safari
786 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
787 v = (v < 10) ? 10 : v;
788 v = (v > 48) ? 48 : v;
789 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
794 v = Math.max(1, v+adjust);
796 this.execCmd('FontSize', v );
799 onEditorEvent : function(e)
803 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
804 return; // we do not handle this.. (undo manager does..)
806 // clicking a 'block'?
808 // in theory this detects if the last element is not a br, then we try and do that.
809 // its so clicking in space at bottom triggers adding a br and moving the cursor.
811 e.target.nodeName == 'BODY' &&
812 e.type == "mouseup" &&
813 this.doc.body.lastChild
815 var lc = this.doc.body.lastChild;
816 // gtx-trans is google translate plugin adding crap.
817 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
818 lc = lc.previousSibling;
820 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
821 // if last element is <BR> - then dont do anything.
823 var ns = this.doc.createElement('br');
824 this.doc.body.appendChild(ns);
825 range = this.doc.createRange();
826 range.setStartAfter(ns);
827 range.collapse(true);
828 var sel = this.win.getSelection();
829 sel.removeAllRanges();
836 this.fireEditorEvent(e);
837 // this.updateToolbar();
838 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
841 fireEditorEvent: function(e)
843 this.owner.fireEvent('editorevent', this, e);
846 insertTag : function(tg)
848 // could be a bit smarter... -> wrap the current selected tRoo..
849 if (tg.toLowerCase() == 'span' ||
850 tg.toLowerCase() == 'code' ||
851 tg.toLowerCase() == 'sup' ||
852 tg.toLowerCase() == 'sub'
855 range = this.createRange(this.getSelection());
856 var wrappingNode = this.doc.createElement(tg.toLowerCase());
857 wrappingNode.appendChild(range.extractContents());
858 range.insertNode(wrappingNode);
865 this.execCmd("formatblock", tg);
866 this.undoManager.addEvent();
869 insertText : function(txt)
873 var range = this.createRange();
874 range.deleteContents();
875 //alert(Sender.getAttribute('label'));
877 range.insertNode(this.doc.createTextNode(txt));
878 this.undoManager.addEvent();
884 * Executes a Midas editor command on the editor document and performs necessary focus and
885 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
886 * @param {String} cmd The Midas command
887 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
889 relayCmd : function(cmd, value)
895 case 'justifycenter':
896 // if we are in a cell, then we will adjust the
897 var n = this.getParentElement();
898 var td = n.closest('td');
900 var bl = Roo.htmleditor.Block.factory(td);
901 bl.textAlign = cmd.replace('justify','');
903 this.owner.fireEvent('editorevent', this);
906 this.execCmd('styleWithCSS', true); //
910 // if there is no selection, then we insert, and set the curson inside it..
911 this.execCmd('styleWithCSS', false);
921 this.execCmd(cmd, value);
922 this.owner.fireEvent('editorevent', this);
923 //this.updateToolbar();
924 this.owner.deferFocus();
928 * Executes a Midas editor command directly on the editor document.
929 * For visual commands, you should use {@link #relayCmd} instead.
930 * <b>This should only be called after the editor is initialized.</b>
931 * @param {String} cmd The Midas command
932 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
934 execCmd : function(cmd, value){
935 this.doc.execCommand(cmd, false, value === undefined ? null : value);
942 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
944 * @param {String} text | dom node..
946 insertAtCursor : function(text)
953 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
957 // from jquery ui (MIT licenced)
961 if (win.getSelection && win.getSelection().getRangeAt) {
963 // delete the existing?
965 this.createRange(this.getSelection()).deleteContents();
966 range = win.getSelection().getRangeAt(0);
967 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
968 range.insertNode(node);
969 range = range.cloneRange();
970 range.collapse(false);
972 win.getSelection().removeAllRanges();
973 win.getSelection().addRange(range);
977 } else if (win.document.selection && win.document.selection.createRange) {
978 // no firefox support
979 var txt = typeof(text) == 'string' ? text : text.outerHTML;
980 win.document.selection.createRange().pasteHTML(txt);
983 // no firefox support
984 var txt = typeof(text) == 'string' ? text : text.outerHTML;
985 this.execCmd('InsertHTML', txt);
993 mozKeyPress : function(e){
995 var c = e.getCharCode(), cmd;
998 c = String.fromCharCode(c).toLowerCase();
1012 // this.cleanUpPaste.defer(100, this);
1020 //this.execCmd(cmd);
1021 //this.deferFocus();
1030 fixKeys : function(){ // load time branching for fastest keydown performance
1035 var k = e.getKey(), r;
1038 r = this.doc.selection.createRange();
1041 r.pasteHTML('    ');
1046 /// this is handled by Roo.htmleditor.KeyEnter
1049 r = this.doc.selection.createRange();
1051 var target = r.parentElement();
1052 if(!target || target.tagName.toLowerCase() != 'li'){
1054 r.pasteHTML('<br/>');
1061 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1062 // this.cleanUpPaste.defer(100, this);
1068 }else if(Roo.isOpera){
1074 this.execCmd('InsertHTML','    ');
1078 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1079 // this.cleanUpPaste.defer(100, this);
1084 }else if(Roo.isSafari){
1090 this.execCmd('InsertText','\t');
1094 this.mozKeyPress(e);
1096 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1097 // this.cleanUpPaste.defer(100, this);
1105 getAllAncestors: function()
1107 var p = this.getSelectedNode();
1110 a.push(p); // push blank onto stack..
1111 p = this.getParentElement();
1115 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1119 a.push(this.doc.body);
1123 lastSelNode : false,
1126 getSelection : function()
1128 this.assignDocWin();
1129 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1133 * @param {DomElement} node the node to select
1135 selectNode : function(node, collapse)
1137 var nodeRange = node.ownerDocument.createRange();
1139 nodeRange.selectNode(node);
1141 nodeRange.selectNodeContents(node);
1143 if (collapse === true) {
1144 nodeRange.collapse(true);
1147 var s = this.win.getSelection();
1148 s.removeAllRanges();
1149 s.addRange(nodeRange);
1152 getSelectedNode: function()
1154 // this may only work on Gecko!!!
1156 // should we cache this!!!!
1160 var range = this.createRange(this.getSelection()).cloneRange();
1163 var parent = range.parentElement();
1165 var testRange = range.duplicate();
1166 testRange.moveToElementText(parent);
1167 if (testRange.inRange(range)) {
1170 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1173 parent = parent.parentElement;
1178 // is ancestor a text element.
1179 var ac = range.commonAncestorContainer;
1180 if (ac.nodeType == 3) {
1184 var ar = ac.childNodes;
1187 var other_nodes = [];
1188 var has_other_nodes = false;
1189 for (var i=0;i<ar.length;i++) {
1190 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1193 // fullly contained node.
1195 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1200 // probably selected..
1201 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1202 other_nodes.push(ar[i]);
1206 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1211 has_other_nodes = true;
1213 if (!nodes.length && other_nodes.length) {
1216 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1224 createRange: function(sel)
1226 // this has strange effects when using with
1227 // top toolbar - not sure if it's a great idea.
1228 //this.editor.contentWindow.focus();
1229 if (typeof sel != "undefined") {
1231 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1233 return this.doc.createRange();
1236 return this.doc.createRange();
1239 getParentElement: function()
1242 this.assignDocWin();
1243 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1245 var range = this.createRange(sel);
1248 var p = range.commonAncestorContainer;
1249 while (p.nodeType == 3) { // text node
1260 * Range intersection.. the hard stuff...
1264 * [ -- selected range --- ]
1268 * if end is before start or hits it. fail.
1269 * if start is after end or hits it fail.
1271 * if either hits (but other is outside. - then it's not
1277 // @see http://www.thismuchiknow.co.uk/?p=64.
1278 rangeIntersectsNode : function(range, node)
1280 var nodeRange = node.ownerDocument.createRange();
1282 nodeRange.selectNode(node);
1284 nodeRange.selectNodeContents(node);
1287 var rangeStartRange = range.cloneRange();
1288 rangeStartRange.collapse(true);
1290 var rangeEndRange = range.cloneRange();
1291 rangeEndRange.collapse(false);
1293 var nodeStartRange = nodeRange.cloneRange();
1294 nodeStartRange.collapse(true);
1296 var nodeEndRange = nodeRange.cloneRange();
1297 nodeEndRange.collapse(false);
1299 return rangeStartRange.compareBoundaryPoints(
1300 Range.START_TO_START, nodeEndRange) == -1 &&
1301 rangeEndRange.compareBoundaryPoints(
1302 Range.START_TO_START, nodeStartRange) == 1;
1306 rangeCompareNode : function(range, node)
1308 var nodeRange = node.ownerDocument.createRange();
1310 nodeRange.selectNode(node);
1312 nodeRange.selectNodeContents(node);
1316 range.collapse(true);
1318 nodeRange.collapse(true);
1320 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1321 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1323 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1325 var nodeIsBefore = ss == 1;
1326 var nodeIsAfter = ee == -1;
1328 if (nodeIsBefore && nodeIsAfter) {
1331 if (!nodeIsBefore && nodeIsAfter) {
1332 return 1; //right trailed.
1335 if (nodeIsBefore && !nodeIsAfter) {
1336 return 2; // left trailed.
1342 cleanWordChars : function(input) {// change the chars to hex code
1345 [ 8211, "–" ],
1346 [ 8212, "—" ],
1355 Roo.each(swapCodes, function(sw) {
1356 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1358 output = output.replace(swapper, sw[1]);
1368 cleanUpChild : function (node)
1371 new Roo.htmleditor.FilterComment({node : node});
1372 new Roo.htmleditor.FilterAttributes({
1374 attrib_black : this.ablack,
1375 attrib_clean : this.aclean,
1376 style_white : this.cwhite,
1377 style_black : this.cblack
1379 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1380 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1386 * Clean up MS wordisms...
1387 * @deprecated - use filter directly
1389 cleanWord : function(node)
1391 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1392 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1399 * @deprecated - use filters
1401 cleanTableWidths : function(node)
1403 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1410 applyBlacklists : function()
1412 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1413 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1415 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1416 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1417 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1421 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1422 if (b.indexOf(tag) > -1) {
1425 this.white.push(tag);
1429 Roo.each(w, function(tag) {
1430 if (b.indexOf(tag) > -1) {
1433 if (this.white.indexOf(tag) > -1) {
1436 this.white.push(tag);
1441 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1442 if (w.indexOf(tag) > -1) {
1445 this.black.push(tag);
1449 Roo.each(b, function(tag) {
1450 if (w.indexOf(tag) > -1) {
1453 if (this.black.indexOf(tag) > -1) {
1456 this.black.push(tag);
1461 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1462 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1466 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1467 if (b.indexOf(tag) > -1) {
1470 this.cwhite.push(tag);
1474 Roo.each(w, function(tag) {
1475 if (b.indexOf(tag) > -1) {
1478 if (this.cwhite.indexOf(tag) > -1) {
1481 this.cwhite.push(tag);
1486 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1487 if (w.indexOf(tag) > -1) {
1490 this.cblack.push(tag);
1494 Roo.each(b, function(tag) {
1495 if (w.indexOf(tag) > -1) {
1498 if (this.cblack.indexOf(tag) > -1) {
1501 this.cblack.push(tag);
1506 setStylesheets : function(stylesheets)
1508 if(typeof(stylesheets) == 'string'){
1509 Roo.get(this.iframe.contentDocument.head).createChild({
1520 Roo.each(stylesheets, function(s) {
1525 Roo.get(_this.iframe.contentDocument.head).createChild({
1537 updateLanguage : function()
1539 if (!this.iframe || !this.iframe.contentDocument) {
1542 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1546 removeStylesheets : function()
1550 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1555 setStyle : function(style)
1557 Roo.get(this.iframe.contentDocument.head).createChild({
1566 // hide stuff that is not compatible
1584 * @cfg {String} fieldClass @hide
1587 * @cfg {String} focusClass @hide
1590 * @cfg {String} autoCreate @hide
1593 * @cfg {String} inputType @hide
1596 * @cfg {String} invalidClass @hide
1599 * @cfg {String} invalidText @hide
1602 * @cfg {String} msgFx @hide
1605 * @cfg {String} validateOnBlur @hide
1609 Roo.HtmlEditorCore.white = [
1610 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1612 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1613 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1614 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1615 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1616 'TABLE', 'UL', 'XMP',
1618 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1621 'DIR', 'MENU', 'OL', 'UL', 'DL',
1627 Roo.HtmlEditorCore.black = [
1628 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1630 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1631 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1632 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1633 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1634 //'FONT' // CLEAN LATER..
1635 'COLGROUP', 'COL' // messy tables.
1639 Roo.HtmlEditorCore.clean = [ // ?? needed???
1640 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1642 Roo.HtmlEditorCore.tag_remove = [
1647 Roo.HtmlEditorCore.ablack = [
1651 Roo.HtmlEditorCore.aclean = [
1652 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1656 Roo.HtmlEditorCore.pwhite= [
1657 'http', 'https', 'mailto'
1660 // white listed style attributes.
1661 Roo.HtmlEditorCore.cwhite= [
1662 // 'text-align', /// default is to allow most things..
1668 // black listed style attributes.
1669 Roo.HtmlEditorCore.cblack= [
1670 // 'font-size' -- this can be set by the project