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({
397 'data-caption-display',
410 attrib_clean : ['href', 'src' ]
413 var tidy = new Roo.htmleditor.TidySerializer({
416 html = tidy.serialize(div);
422 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
423 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
425 html = '<div style="'+m[0]+'">' + html + '</div>';
428 html = this.cleanHtml(html);
429 // fix up the special chars.. normaly like back quotes in word...
430 // however we do not want to do this with chinese..
431 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
433 var cc = match.charCodeAt();
435 // Get the character value, handling surrogate pairs
436 if (match.length == 2) {
437 // It's a surrogate pair, calculate the Unicode code point
438 var high = match.charCodeAt(0) - 0xD800;
439 var low = match.charCodeAt(1) - 0xDC00;
440 cc = (high * 0x400) + low + 0x10000;
442 (cc >= 0x4E00 && cc < 0xA000 ) ||
443 (cc >= 0x3400 && cc < 0x4E00 ) ||
444 (cc >= 0xf900 && cc < 0xfb00 )
449 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
450 return "&#" + cc + ";";
457 if(this.owner.fireEvent('beforesync', this, html) !== false){
458 this.el.dom.value = html;
459 this.owner.fireEvent('sync', this, html);
465 * TEXTAREA -> EDITABLE
466 * Protected method that will not generally be called directly. Pushes the value of the textarea
467 * into the iframe editor.
469 pushValue : function()
471 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
472 if(this.initialized){
473 var v = this.el.dom.value.trim();
476 if(this.owner.fireEvent('beforepush', this, v) !== false){
477 var d = (this.doc.body || this.doc.documentElement);
480 this.el.dom.value = d.innerHTML;
481 this.owner.fireEvent('push', this, v);
483 if (this.autoClean) {
484 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
485 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
487 if (this.enableBlocks) {
488 Roo.htmleditor.Block.initAll(this.doc.body);
491 this.updateLanguage();
493 var lc = this.doc.body.lastChild;
494 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
495 // add an extra line at the end.
496 this.doc.body.appendChild(this.doc.createElement('br'));
504 deferFocus : function(){
505 this.focus.defer(10, this);
510 if(this.win && !this.sourceEditMode){
517 assignDocWin: function()
519 var iframe = this.iframe;
522 this.doc = iframe.contentWindow.document;
523 this.win = iframe.contentWindow;
525 // if (!Roo.get(this.frameId)) {
528 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
529 // this.win = Roo.get(this.frameId).dom.contentWindow;
531 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
535 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
536 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
541 initEditor : function(){
542 //console.log("INIT EDITOR");
547 this.doc.designMode="on";
549 this.doc.write(this.getDocMarkup());
552 var dbody = (this.doc.body || this.doc.documentElement);
553 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
554 // this copies styles from the containing element into thsi one..
555 // not sure why we need all of this..
556 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
558 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
559 //ss['background-attachment'] = 'fixed'; // w3c
560 dbody.bgProperties = 'fixed'; // ie
561 dbody.setAttribute("translate", "no");
563 //Roo.DomHelper.applyStyles(dbody, ss);
564 Roo.EventManager.on(this.doc, {
566 'mouseup': this.onEditorEvent,
567 'dblclick': this.onEditorEvent,
568 'click': this.onEditorEvent,
569 'keyup': this.onEditorEvent,
574 Roo.EventManager.on(this.doc, {
575 'paste': this.onPasteEvent,
579 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
582 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
583 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
585 this.initialized = true;
588 // initialize special key events - enter
589 new Roo.htmleditor.KeyEnter({core : this});
593 this.owner.fireEvent('initialize', this);
596 // this is to prevent a href clicks resulting in a redirect?
598 onPasteEvent : function(e,v)
600 // I think we better assume paste is going to be a dirty load of rubish from word..
602 // even pasting into a 'email version' of this widget will have to clean up that mess.
603 var cd = (e.browserEvent.clipboardData || window.clipboardData);
605 // check what type of paste - if it's an image, then handle it differently.
606 if (cd.files && cd.files.length > 0 && cd.types.indexOf('text/html') < 0) {
608 var urlAPI = (window.createObjectURL && window) ||
609 (window.URL && URL.revokeObjectURL && URL) ||
610 (window.webkitURL && webkitURL);
612 var r = new FileReader();
614 r.addEventListener('load',function()
617 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
619 if (t.enableBlocks) {
621 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
622 if (img.closest('figure')) { // assume!! that it's aready
625 var fig = new Roo.htmleditor.BlockFigure({
628 fig.updateElement(img); // replace it..
632 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
633 t.owner.fireEvent('paste', this);
635 r.readAsDataURL(cd.files[0]);
641 if (cd.types.indexOf('text/html') < 0 ) {
645 var html = cd.getData('text/html'); // clipboard event
646 if (cd.types.indexOf('text/rtf') > -1) {
647 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
648 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
653 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
654 .map(function(g) { return g.toDataURL(); })
655 .filter(function(g) { return g != 'about:blank'; });
658 html = this.cleanWordChars(html);
660 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
663 var sn = this.getParentElement();
664 // check if d contains a table, and prevent nesting??
665 //Roo.log(d.getElementsByTagName('table'));
667 //Roo.log(sn.closest('table'));
668 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
670 this.insertAtCursor("You can not nest tables");
671 //Roo.log("prevent?"); // fixme -
677 if (images.length > 0) {
678 // replace all v:imagedata - with img.
679 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
680 Roo.each(ar, function(node) {
681 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
682 node.parentNode.removeChild(node);
686 Roo.each(d.getElementsByTagName('img'), function(img, i) {
687 img.setAttribute('src', images[i]);
690 if (this.autoClean) {
691 new Roo.htmleditor.FilterWord({ node : d });
693 new Roo.htmleditor.FilterStyleToTag({ node : d });
694 new Roo.htmleditor.FilterAttributes({
703 /* THESE ARE NOT ALLWOED FOR PASTE
705 'data-caption-display',
719 attrib_clean : ['href', 'src' ]
721 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
723 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
724 new Roo.htmleditor.FilterParagraph({ node : d });
725 new Roo.htmleditor.FilterSpan({ node : d });
726 new Roo.htmleditor.FilterLongBr({ node : d });
727 new Roo.htmleditor.FilterComment({ node : d });
731 if (this.enableBlocks) {
733 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
734 if (img.closest('figure')) { // assume!! that it's aready
737 var fig = new Roo.htmleditor.BlockFigure({
740 fig.updateElement(img); // replace it..
746 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
747 if (this.enableBlocks) {
748 Roo.htmleditor.Block.initAll(this.doc.body);
753 this.owner.fireEvent('paste', this);
755 // default behaveiour should be our local cleanup paste? (optional?)
756 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
757 //this.owner.fireEvent('paste', e, v);
760 onDestroy : function(){
766 //for (var i =0; i < this.toolbars.length;i++) {
767 // // fixme - ask toolbars for heights?
768 // this.toolbars[i].onDestroy();
771 //this.wrap.dom.innerHTML = '';
772 //this.wrap.remove();
777 onFirstFocus : function(){
780 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
782 this.activated = true;
785 if(Roo.isGecko){ // prevent silly gecko errors
787 var s = this.win.getSelection();
788 if(!s.focusNode || s.focusNode.nodeType != 3){
789 var r = s.getRangeAt(0);
790 r.selectNodeContents((this.doc.body || this.doc.documentElement));
795 this.execCmd('useCSS', true);
796 this.execCmd('styleWithCSS', false);
799 this.owner.fireEvent('activate', this);
803 adjustFont: function(btn){
804 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
805 //if(Roo.isSafari){ // safari
808 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
809 if(Roo.isSafari){ // safari
810 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
811 v = (v < 10) ? 10 : v;
812 v = (v > 48) ? 48 : v;
813 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
818 v = Math.max(1, v+adjust);
820 this.execCmd('FontSize', v );
823 onEditorEvent : function(e)
827 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
828 return; // we do not handle this.. (undo manager does..)
830 // clicking a 'block'?
832 // in theory this detects if the last element is not a br, then we try and do that.
833 // its so clicking in space at bottom triggers adding a br and moving the cursor.
835 e.target.nodeName == 'BODY' &&
836 e.type == "mouseup" &&
837 this.doc.body.lastChild
839 var lc = this.doc.body.lastChild;
840 // gtx-trans is google translate plugin adding crap.
841 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
842 lc = lc.previousSibling;
844 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
845 // if last element is <BR> - then dont do anything.
847 var ns = this.doc.createElement('br');
848 this.doc.body.appendChild(ns);
849 range = this.doc.createRange();
850 range.setStartAfter(ns);
851 range.collapse(true);
852 var sel = this.win.getSelection();
853 sel.removeAllRanges();
860 this.fireEditorEvent(e);
861 // this.updateToolbar();
862 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
865 fireEditorEvent: function(e)
867 this.owner.fireEvent('editorevent', this, e);
870 insertTag : function(tg)
872 // could be a bit smarter... -> wrap the current selected tRoo..
873 if (tg.toLowerCase() == 'span' ||
874 tg.toLowerCase() == 'code' ||
875 tg.toLowerCase() == 'sup' ||
876 tg.toLowerCase() == 'sub'
879 range = this.createRange(this.getSelection());
880 var wrappingNode = this.doc.createElement(tg.toLowerCase());
881 wrappingNode.appendChild(range.extractContents());
882 range.insertNode(wrappingNode);
889 this.execCmd("formatblock", tg);
890 this.undoManager.addEvent();
893 insertText : function(txt)
897 var range = this.createRange();
898 range.deleteContents();
899 //alert(Sender.getAttribute('label'));
901 range.insertNode(this.doc.createTextNode(txt));
902 this.undoManager.addEvent();
908 * Executes a Midas editor command on the editor document and performs necessary focus and
909 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
910 * @param {String} cmd The Midas command
911 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
913 relayCmd : function(cmd, value)
919 case 'justifycenter':
920 // if we are in a cell, then we will adjust the
921 var n = this.getParentElement();
922 var td = n.closest('td');
924 var bl = Roo.htmleditor.Block.factory(td);
925 bl.textAlign = cmd.replace('justify','');
927 this.owner.fireEvent('editorevent', this);
930 this.execCmd('styleWithCSS', true); //
935 // if there is no selection, then we insert, and set the curson inside it..
936 this.execCmd('styleWithCSS', false);
946 this.execCmd(cmd, value);
947 this.owner.fireEvent('editorevent', this);
948 //this.updateToolbar();
949 this.owner.deferFocus();
953 * Executes a Midas editor command directly on the editor document.
954 * For visual commands, you should use {@link #relayCmd} instead.
955 * <b>This should only be called after the editor is initialized.</b>
956 * @param {String} cmd The Midas command
957 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
959 execCmd : function(cmd, value){
960 this.doc.execCommand(cmd, false, value === undefined ? null : value);
967 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
969 * @param {String} text | dom node..
971 insertAtCursor : function(text)
978 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
982 // from jquery ui (MIT licenced)
986 if (win.getSelection && win.getSelection().getRangeAt) {
988 // delete the existing?
990 this.createRange(this.getSelection()).deleteContents();
991 range = win.getSelection().getRangeAt(0);
992 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
993 range.insertNode(node);
994 range = range.cloneRange();
995 range.collapse(false);
997 win.getSelection().removeAllRanges();
998 win.getSelection().addRange(range);
1002 } else if (win.document.selection && win.document.selection.createRange) {
1003 // no firefox support
1004 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1005 win.document.selection.createRange().pasteHTML(txt);
1008 // no firefox support
1009 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1010 this.execCmd('InsertHTML', txt);
1018 mozKeyPress : function(e){
1020 var c = e.getCharCode(), cmd;
1023 c = String.fromCharCode(c).toLowerCase();
1037 // this.cleanUpPaste.defer(100, this);
1045 //this.execCmd(cmd);
1046 //this.deferFocus();
1055 fixKeys : function(){ // load time branching for fastest keydown performance
1060 var k = e.getKey(), r;
1063 r = this.doc.selection.createRange();
1066 r.pasteHTML('    ');
1071 /// this is handled by Roo.htmleditor.KeyEnter
1074 r = this.doc.selection.createRange();
1076 var target = r.parentElement();
1077 if(!target || target.tagName.toLowerCase() != 'li'){
1079 r.pasteHTML('<br/>');
1086 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1087 // this.cleanUpPaste.defer(100, this);
1093 }else if(Roo.isOpera){
1099 this.execCmd('InsertHTML','    ');
1103 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1104 // this.cleanUpPaste.defer(100, this);
1109 }else if(Roo.isSafari){
1115 this.execCmd('InsertText','\t');
1119 this.mozKeyPress(e);
1121 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1122 // this.cleanUpPaste.defer(100, this);
1130 getAllAncestors: function()
1132 var p = this.getSelectedNode();
1135 a.push(p); // push blank onto stack..
1136 p = this.getParentElement();
1140 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1144 a.push(this.doc.body);
1148 lastSelNode : false,
1151 getSelection : function()
1153 this.assignDocWin();
1154 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1158 * @param {DomElement} node the node to select
1160 selectNode : function(node, collapse)
1162 var nodeRange = node.ownerDocument.createRange();
1164 nodeRange.selectNode(node);
1166 nodeRange.selectNodeContents(node);
1168 if (collapse === true) {
1169 nodeRange.collapse(true);
1172 var s = this.win.getSelection();
1173 s.removeAllRanges();
1174 s.addRange(nodeRange);
1177 getSelectedNode: function()
1179 // this may only work on Gecko!!!
1181 // should we cache this!!!!
1185 var range = this.createRange(this.getSelection()).cloneRange();
1188 var parent = range.parentElement();
1190 var testRange = range.duplicate();
1191 testRange.moveToElementText(parent);
1192 if (testRange.inRange(range)) {
1195 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1198 parent = parent.parentElement;
1203 // is ancestor a text element.
1204 var ac = range.commonAncestorContainer;
1205 if (ac.nodeType == 3) {
1209 var ar = ac.childNodes;
1212 var other_nodes = [];
1213 var has_other_nodes = false;
1214 for (var i=0;i<ar.length;i++) {
1215 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1218 // fullly contained node.
1220 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1225 // probably selected..
1226 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1227 other_nodes.push(ar[i]);
1231 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1236 has_other_nodes = true;
1238 if (!nodes.length && other_nodes.length) {
1241 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1249 createRange: function(sel)
1251 // this has strange effects when using with
1252 // top toolbar - not sure if it's a great idea.
1253 //this.editor.contentWindow.focus();
1254 if (typeof sel != "undefined") {
1256 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1258 return this.doc.createRange();
1261 return this.doc.createRange();
1264 getParentElement: function()
1267 this.assignDocWin();
1268 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1270 var range = this.createRange(sel);
1273 var p = range.commonAncestorContainer;
1274 while (p.nodeType == 3) { // text node
1285 * Range intersection.. the hard stuff...
1289 * [ -- selected range --- ]
1293 * if end is before start or hits it. fail.
1294 * if start is after end or hits it fail.
1296 * if either hits (but other is outside. - then it's not
1302 // @see http://www.thismuchiknow.co.uk/?p=64.
1303 rangeIntersectsNode : function(range, node)
1305 var nodeRange = node.ownerDocument.createRange();
1307 nodeRange.selectNode(node);
1309 nodeRange.selectNodeContents(node);
1312 var rangeStartRange = range.cloneRange();
1313 rangeStartRange.collapse(true);
1315 var rangeEndRange = range.cloneRange();
1316 rangeEndRange.collapse(false);
1318 var nodeStartRange = nodeRange.cloneRange();
1319 nodeStartRange.collapse(true);
1321 var nodeEndRange = nodeRange.cloneRange();
1322 nodeEndRange.collapse(false);
1324 return rangeStartRange.compareBoundaryPoints(
1325 Range.START_TO_START, nodeEndRange) == -1 &&
1326 rangeEndRange.compareBoundaryPoints(
1327 Range.START_TO_START, nodeStartRange) == 1;
1331 rangeCompareNode : function(range, node)
1333 var nodeRange = node.ownerDocument.createRange();
1335 nodeRange.selectNode(node);
1337 nodeRange.selectNodeContents(node);
1341 range.collapse(true);
1343 nodeRange.collapse(true);
1345 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1346 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1348 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1350 var nodeIsBefore = ss == 1;
1351 var nodeIsAfter = ee == -1;
1353 if (nodeIsBefore && nodeIsAfter) {
1356 if (!nodeIsBefore && nodeIsAfter) {
1357 return 1; //right trailed.
1360 if (nodeIsBefore && !nodeIsAfter) {
1361 return 2; // left trailed.
1367 cleanWordChars : function(input) {// change the chars to hex code
1370 [ 8211, "–" ],
1371 [ 8212, "—" ],
1380 Roo.each(swapCodes, function(sw) {
1381 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1383 output = output.replace(swapper, sw[1]);
1393 cleanUpChild : function (node)
1396 new Roo.htmleditor.FilterComment({node : node});
1397 new Roo.htmleditor.FilterAttributes({
1399 attrib_black : this.ablack,
1400 attrib_clean : this.aclean,
1401 style_white : this.cwhite,
1402 style_black : this.cblack
1404 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1405 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1411 * Clean up MS wordisms...
1412 * @deprecated - use filter directly
1414 cleanWord : function(node)
1416 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1417 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1424 * @deprecated - use filters
1426 cleanTableWidths : function(node)
1428 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1435 applyBlacklists : function()
1437 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1438 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1440 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1441 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1442 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1446 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1447 if (b.indexOf(tag) > -1) {
1450 this.white.push(tag);
1454 Roo.each(w, function(tag) {
1455 if (b.indexOf(tag) > -1) {
1458 if (this.white.indexOf(tag) > -1) {
1461 this.white.push(tag);
1466 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1467 if (w.indexOf(tag) > -1) {
1470 this.black.push(tag);
1474 Roo.each(b, function(tag) {
1475 if (w.indexOf(tag) > -1) {
1478 if (this.black.indexOf(tag) > -1) {
1481 this.black.push(tag);
1486 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1487 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1491 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1492 if (b.indexOf(tag) > -1) {
1495 this.cwhite.push(tag);
1499 Roo.each(w, function(tag) {
1500 if (b.indexOf(tag) > -1) {
1503 if (this.cwhite.indexOf(tag) > -1) {
1506 this.cwhite.push(tag);
1511 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1512 if (w.indexOf(tag) > -1) {
1515 this.cblack.push(tag);
1519 Roo.each(b, function(tag) {
1520 if (w.indexOf(tag) > -1) {
1523 if (this.cblack.indexOf(tag) > -1) {
1526 this.cblack.push(tag);
1531 setStylesheets : function(stylesheets)
1533 if(typeof(stylesheets) == 'string'){
1534 Roo.get(this.iframe.contentDocument.head).createChild({
1545 Roo.each(stylesheets, function(s) {
1550 Roo.get(_this.iframe.contentDocument.head).createChild({
1562 updateLanguage : function()
1564 if (!this.iframe || !this.iframe.contentDocument) {
1567 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1571 removeStylesheets : function()
1575 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1580 setStyle : function(style)
1582 Roo.get(this.iframe.contentDocument.head).createChild({
1591 // hide stuff that is not compatible
1609 * @cfg {String} fieldClass @hide
1612 * @cfg {String} focusClass @hide
1615 * @cfg {String} autoCreate @hide
1618 * @cfg {String} inputType @hide
1621 * @cfg {String} invalidClass @hide
1624 * @cfg {String} invalidText @hide
1627 * @cfg {String} msgFx @hide
1630 * @cfg {String} validateOnBlur @hide
1634 Roo.HtmlEditorCore.white = [
1635 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1637 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1638 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1639 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1640 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1641 'TABLE', 'UL', 'XMP',
1643 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1646 'DIR', 'MENU', 'OL', 'UL', 'DL',
1652 Roo.HtmlEditorCore.black = [
1653 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1655 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1656 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1657 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1658 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1659 //'FONT' // CLEAN LATER..
1660 'COLGROUP', 'COL' // messy tables.
1664 Roo.HtmlEditorCore.clean = [ // ?? needed???
1665 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1667 Roo.HtmlEditorCore.tag_remove = [
1672 Roo.HtmlEditorCore.ablack = [
1676 Roo.HtmlEditorCore.aclean = [
1677 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1681 Roo.HtmlEditorCore.pwhite= [
1682 'http', 'https', 'mailto'
1685 // white listed style attributes.
1686 Roo.HtmlEditorCore.cwhite= [
1687 // 'text-align', /// default is to allow most things..
1693 // black listed style attributes.
1694 Roo.HtmlEditorCore.cblack= [
1695 // 'font-size' -- this can be set by the project