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 Roo.htmleditor.Block.initAll(this.doc.body);
380 new Roo.htmleditor.FilterBlock({ node : div });
383 var html = div.innerHTML;
386 if (this.autoClean) {
388 new Roo.htmleditor.FilterAttributes({
398 'data-caption-display',
411 attrib_clean : ['href', 'src' ]
414 var tidy = new Roo.htmleditor.TidySerializer({
417 html = tidy.serialize(div);
423 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
424 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
426 html = '<div style="'+m[0]+'">' + html + '</div>';
429 html = this.cleanHtml(html);
430 // fix up the special chars.. normaly like back quotes in word...
431 // however we do not want to do this with chinese..
432 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
434 var cc = match.charCodeAt();
436 // Get the character value, handling surrogate pairs
437 if (match.length == 2) {
438 // It's a surrogate pair, calculate the Unicode code point
439 var high = match.charCodeAt(0) - 0xD800;
440 var low = match.charCodeAt(1) - 0xDC00;
441 cc = (high * 0x400) + low + 0x10000;
443 (cc >= 0x4E00 && cc < 0xA000 ) ||
444 (cc >= 0x3400 && cc < 0x4E00 ) ||
445 (cc >= 0xf900 && cc < 0xfb00 )
450 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
451 return "&#" + cc + ";";
458 if(this.owner.fireEvent('beforesync', this, html) !== false){
459 this.el.dom.value = html;
460 this.owner.fireEvent('sync', this, html);
466 * TEXTAREA -> EDITABLE
467 * Protected method that will not generally be called directly. Pushes the value of the textarea
468 * into the iframe editor.
470 pushValue : function()
472 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
473 if(this.initialized){
474 var v = this.el.dom.value.trim();
477 if(this.owner.fireEvent('beforepush', this, v) !== false){
478 var d = (this.doc.body || this.doc.documentElement);
481 this.el.dom.value = d.innerHTML;
482 this.owner.fireEvent('push', this, v);
484 if (this.autoClean) {
485 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
486 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
488 if (this.enableBlocks) {
489 Roo.htmleditor.Block.initAll(this.doc.body);
492 this.updateLanguage();
494 var lc = this.doc.body.lastChild;
495 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
496 // add an extra line at the end.
497 this.doc.body.appendChild(this.doc.createElement('br'));
505 deferFocus : function(){
506 this.focus.defer(10, this);
511 if(this.win && !this.sourceEditMode){
518 assignDocWin: function()
520 var iframe = this.iframe;
523 this.doc = iframe.contentWindow.document;
524 this.win = iframe.contentWindow;
526 // if (!Roo.get(this.frameId)) {
529 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
530 // this.win = Roo.get(this.frameId).dom.contentWindow;
532 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
536 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
537 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
542 initEditor : function(){
543 //console.log("INIT EDITOR");
548 this.doc.designMode="on";
550 this.doc.write(this.getDocMarkup());
553 var dbody = (this.doc.body || this.doc.documentElement);
554 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
555 // this copies styles from the containing element into thsi one..
556 // not sure why we need all of this..
557 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
559 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
560 //ss['background-attachment'] = 'fixed'; // w3c
561 dbody.bgProperties = 'fixed'; // ie
562 dbody.setAttribute("translate", "no");
564 //Roo.DomHelper.applyStyles(dbody, ss);
565 Roo.EventManager.on(this.doc, {
567 'mouseup': this.onEditorEvent,
568 'dblclick': this.onEditorEvent,
569 'click': this.onEditorEvent,
570 'keyup': this.onEditorEvent,
575 Roo.EventManager.on(this.doc, {
576 'paste': this.onPasteEvent,
580 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
583 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
584 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
586 this.initialized = true;
589 // initialize special key events - enter
590 new Roo.htmleditor.KeyEnter({core : this});
594 this.owner.fireEvent('initialize', this);
597 // this is to prevent a href clicks resulting in a redirect?
599 onPasteEvent : function(e,v)
601 // I think we better assume paste is going to be a dirty load of rubish from word..
603 // even pasting into a 'email version' of this widget will have to clean up that mess.
604 var cd = (e.browserEvent.clipboardData || window.clipboardData);
606 // check what type of paste - if it's an image, then handle it differently.
607 if (cd.files && cd.files.length > 0 && cd.types.indexOf('text/html') < 0) {
609 var urlAPI = (window.createObjectURL && window) ||
610 (window.URL && URL.revokeObjectURL && URL) ||
611 (window.webkitURL && webkitURL);
613 var r = new FileReader();
615 r.addEventListener('load',function()
618 var d = (new DOMParser().parseFromString('<img src="' + r.result+ '">', 'text/html')).body;
620 if (t.enableBlocks) {
622 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
623 if (img.closest('figure')) { // assume!! that it's aready
626 var fig = new Roo.htmleditor.BlockFigure({
629 fig.updateElement(img); // replace it..
633 t.insertAtCursor(d.innerHTML.replace(/ /g,' '));
634 t.owner.fireEvent('paste', this);
636 r.readAsDataURL(cd.files[0]);
642 if (cd.types.indexOf('text/html') < 0 ) {
646 var html = cd.getData('text/html'); // clipboard event
647 if (cd.types.indexOf('text/rtf') > -1) {
648 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
649 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
654 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
655 .map(function(g) { return g.toDataURL(); })
656 .filter(function(g) { return g != 'about:blank'; });
659 html = this.cleanWordChars(html);
661 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
664 var sn = this.getParentElement();
665 // check if d contains a table, and prevent nesting??
666 //Roo.log(d.getElementsByTagName('table'));
668 //Roo.log(sn.closest('table'));
669 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
671 this.insertAtCursor("You can not nest tables");
672 //Roo.log("prevent?"); // fixme -
678 if (images.length > 0) {
679 // replace all v:imagedata - with img.
680 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
681 Roo.each(ar, function(node) {
682 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
683 node.parentNode.removeChild(node);
687 Roo.each(d.getElementsByTagName('img'), function(img, i) {
688 img.setAttribute('src', images[i]);
691 if (this.autoClean) {
692 new Roo.htmleditor.FilterWord({ node : d });
694 new Roo.htmleditor.FilterStyleToTag({ node : d });
695 new Roo.htmleditor.FilterAttributes({
704 /* THESE ARE NOT ALLWOED FOR PASTE
706 'data-caption-display',
720 attrib_clean : ['href', 'src' ]
722 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
724 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
725 new Roo.htmleditor.FilterParagraph({ node : d });
726 new Roo.htmleditor.FilterHashLink({node : d});
727 new Roo.htmleditor.FilterSpan({ node : d });
728 new Roo.htmleditor.FilterLongBr({ node : d });
729 new Roo.htmleditor.FilterComment({ node : d });
733 if (this.enableBlocks) {
735 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
736 if (img.closest('figure')) { // assume!! that it's aready
739 var fig = new Roo.htmleditor.BlockFigure({
742 fig.updateElement(img); // replace it..
748 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
749 if (this.enableBlocks) {
750 Roo.htmleditor.Block.initAll(this.doc.body);
755 this.owner.fireEvent('paste', this);
757 // default behaveiour should be our local cleanup paste? (optional?)
758 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
759 //this.owner.fireEvent('paste', e, v);
762 onDestroy : function(){
768 //for (var i =0; i < this.toolbars.length;i++) {
769 // // fixme - ask toolbars for heights?
770 // this.toolbars[i].onDestroy();
773 //this.wrap.dom.innerHTML = '';
774 //this.wrap.remove();
779 onFirstFocus : function(){
782 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
784 this.activated = true;
787 if(Roo.isGecko){ // prevent silly gecko errors
789 var s = this.win.getSelection();
790 if(!s.focusNode || s.focusNode.nodeType != 3){
791 var r = s.getRangeAt(0);
792 r.selectNodeContents((this.doc.body || this.doc.documentElement));
797 this.execCmd('useCSS', true);
798 this.execCmd('styleWithCSS', false);
801 this.owner.fireEvent('activate', this);
805 adjustFont: function(btn){
806 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
807 //if(Roo.isSafari){ // safari
810 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
811 if(Roo.isSafari){ // safari
812 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
813 v = (v < 10) ? 10 : v;
814 v = (v > 48) ? 48 : v;
815 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
820 v = Math.max(1, v+adjust);
822 this.execCmd('FontSize', v );
825 onEditorEvent : function(e)
829 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
830 return; // we do not handle this.. (undo manager does..)
832 // clicking a 'block'?
834 // in theory this detects if the last element is not a br, then we try and do that.
835 // its so clicking in space at bottom triggers adding a br and moving the cursor.
837 e.target.nodeName == 'BODY' &&
838 e.type == "mouseup" &&
839 this.doc.body.lastChild
841 var lc = this.doc.body.lastChild;
842 // gtx-trans is google translate plugin adding crap.
843 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
844 lc = lc.previousSibling;
846 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
847 // if last element is <BR> - then dont do anything.
849 var ns = this.doc.createElement('br');
850 this.doc.body.appendChild(ns);
851 range = this.doc.createRange();
852 range.setStartAfter(ns);
853 range.collapse(true);
854 var sel = this.win.getSelection();
855 sel.removeAllRanges();
862 this.fireEditorEvent(e);
863 // this.updateToolbar();
864 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
867 fireEditorEvent: function(e)
869 this.owner.fireEvent('editorevent', this, e);
872 insertTag : function(tg)
874 // could be a bit smarter... -> wrap the current selected tRoo..
875 if (tg.toLowerCase() == 'span' ||
876 tg.toLowerCase() == 'code' ||
877 tg.toLowerCase() == 'sup' ||
878 tg.toLowerCase() == 'sub'
881 range = this.createRange(this.getSelection());
882 var wrappingNode = this.doc.createElement(tg.toLowerCase());
883 wrappingNode.appendChild(range.extractContents());
884 range.insertNode(wrappingNode);
891 this.execCmd("formatblock", tg);
892 this.undoManager.addEvent();
895 insertText : function(txt)
899 var range = this.createRange();
900 range.deleteContents();
901 //alert(Sender.getAttribute('label'));
903 range.insertNode(this.doc.createTextNode(txt));
904 this.undoManager.addEvent();
910 * Executes a Midas editor command on the editor document and performs necessary focus and
911 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
912 * @param {String} cmd The Midas command
913 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
915 relayCmd : function(cmd, value)
921 case 'justifycenter':
922 // if we are in a cell, then we will adjust the
923 var n = this.getParentElement();
924 var td = n.closest('td');
926 var bl = Roo.htmleditor.Block.factory(td);
927 bl.textAlign = cmd.replace('justify','');
929 this.owner.fireEvent('editorevent', this);
932 this.execCmd('styleWithCSS', true); //
937 // if there is no selection, then we insert, and set the curson inside it..
938 this.execCmd('styleWithCSS', false);
948 this.execCmd(cmd, value);
949 this.owner.fireEvent('editorevent', this);
950 //this.updateToolbar();
951 this.owner.deferFocus();
955 * Executes a Midas editor command directly on the editor document.
956 * For visual commands, you should use {@link #relayCmd} instead.
957 * <b>This should only be called after the editor is initialized.</b>
958 * @param {String} cmd The Midas command
959 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
961 execCmd : function(cmd, value){
962 this.doc.execCommand(cmd, false, value === undefined ? null : value);
969 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
971 * @param {String} text | dom node..
973 insertAtCursor : function(text)
980 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
984 // from jquery ui (MIT licenced)
988 if (win.getSelection && win.getSelection().getRangeAt) {
990 // delete the existing?
992 this.createRange(this.getSelection()).deleteContents();
993 range = win.getSelection().getRangeAt(0);
994 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
995 range.insertNode(node);
996 range = range.cloneRange();
997 range.collapse(false);
999 win.getSelection().removeAllRanges();
1000 win.getSelection().addRange(range);
1004 } else if (win.document.selection && win.document.selection.createRange) {
1005 // no firefox support
1006 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1007 win.document.selection.createRange().pasteHTML(txt);
1010 // no firefox support
1011 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1012 this.execCmd('InsertHTML', txt);
1020 mozKeyPress : function(e){
1022 var c = e.getCharCode(), cmd;
1025 c = String.fromCharCode(c).toLowerCase();
1039 // this.cleanUpPaste.defer(100, this);
1047 //this.execCmd(cmd);
1048 //this.deferFocus();
1057 fixKeys : function(){ // load time branching for fastest keydown performance
1062 var k = e.getKey(), r;
1065 r = this.doc.selection.createRange();
1068 r.pasteHTML('    ');
1073 /// this is handled by Roo.htmleditor.KeyEnter
1076 r = this.doc.selection.createRange();
1078 var target = r.parentElement();
1079 if(!target || target.tagName.toLowerCase() != 'li'){
1081 r.pasteHTML('<br/>');
1088 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1089 // this.cleanUpPaste.defer(100, this);
1095 }else if(Roo.isOpera){
1101 this.execCmd('InsertHTML','    ');
1105 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1106 // this.cleanUpPaste.defer(100, this);
1111 }else if(Roo.isSafari){
1117 this.execCmd('InsertText','\t');
1121 this.mozKeyPress(e);
1123 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1124 // this.cleanUpPaste.defer(100, this);
1132 getAllAncestors: function()
1134 var p = this.getSelectedNode();
1137 a.push(p); // push blank onto stack..
1138 p = this.getParentElement();
1142 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1146 a.push(this.doc.body);
1150 lastSelNode : false,
1153 getSelection : function()
1155 this.assignDocWin();
1156 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1160 * @param {DomElement} node the node to select
1162 selectNode : function(node, collapse)
1164 var nodeRange = node.ownerDocument.createRange();
1166 nodeRange.selectNode(node);
1168 nodeRange.selectNodeContents(node);
1170 if (collapse === true) {
1171 nodeRange.collapse(true);
1174 var s = this.win.getSelection();
1175 s.removeAllRanges();
1176 s.addRange(nodeRange);
1179 getSelectedNode: function()
1181 // this may only work on Gecko!!!
1183 // should we cache this!!!!
1187 var range = this.createRange(this.getSelection()).cloneRange();
1190 var parent = range.parentElement();
1192 var testRange = range.duplicate();
1193 testRange.moveToElementText(parent);
1194 if (testRange.inRange(range)) {
1197 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1200 parent = parent.parentElement;
1205 // is ancestor a text element.
1206 var ac = range.commonAncestorContainer;
1207 if (ac.nodeType == 3) {
1211 var ar = ac.childNodes;
1214 var other_nodes = [];
1215 var has_other_nodes = false;
1216 for (var i=0;i<ar.length;i++) {
1217 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1220 // fullly contained node.
1222 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1227 // probably selected..
1228 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1229 other_nodes.push(ar[i]);
1233 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1238 has_other_nodes = true;
1240 if (!nodes.length && other_nodes.length) {
1243 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1251 createRange: function(sel)
1253 // this has strange effects when using with
1254 // top toolbar - not sure if it's a great idea.
1255 //this.editor.contentWindow.focus();
1256 if (typeof sel != "undefined") {
1258 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1260 return this.doc.createRange();
1263 return this.doc.createRange();
1266 getParentElement: function()
1269 this.assignDocWin();
1270 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1272 var range = this.createRange(sel);
1275 var p = range.commonAncestorContainer;
1276 while (p.nodeType == 3) { // text node
1287 * Range intersection.. the hard stuff...
1291 * [ -- selected range --- ]
1295 * if end is before start or hits it. fail.
1296 * if start is after end or hits it fail.
1298 * if either hits (but other is outside. - then it's not
1304 // @see http://www.thismuchiknow.co.uk/?p=64.
1305 rangeIntersectsNode : function(range, node)
1307 var nodeRange = node.ownerDocument.createRange();
1309 nodeRange.selectNode(node);
1311 nodeRange.selectNodeContents(node);
1314 var rangeStartRange = range.cloneRange();
1315 rangeStartRange.collapse(true);
1317 var rangeEndRange = range.cloneRange();
1318 rangeEndRange.collapse(false);
1320 var nodeStartRange = nodeRange.cloneRange();
1321 nodeStartRange.collapse(true);
1323 var nodeEndRange = nodeRange.cloneRange();
1324 nodeEndRange.collapse(false);
1326 return rangeStartRange.compareBoundaryPoints(
1327 Range.START_TO_START, nodeEndRange) == -1 &&
1328 rangeEndRange.compareBoundaryPoints(
1329 Range.START_TO_START, nodeStartRange) == 1;
1333 rangeCompareNode : function(range, node)
1335 var nodeRange = node.ownerDocument.createRange();
1337 nodeRange.selectNode(node);
1339 nodeRange.selectNodeContents(node);
1343 range.collapse(true);
1345 nodeRange.collapse(true);
1347 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1348 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1350 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1352 var nodeIsBefore = ss == 1;
1353 var nodeIsAfter = ee == -1;
1355 if (nodeIsBefore && nodeIsAfter) {
1358 if (!nodeIsBefore && nodeIsAfter) {
1359 return 1; //right trailed.
1362 if (nodeIsBefore && !nodeIsAfter) {
1363 return 2; // left trailed.
1369 cleanWordChars : function(input) {// change the chars to hex code
1372 [ 8211, "–" ],
1373 [ 8212, "—" ],
1382 Roo.each(swapCodes, function(sw) {
1383 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1385 output = output.replace(swapper, sw[1]);
1395 cleanUpChild : function (node)
1398 new Roo.htmleditor.FilterComment({node : node});
1399 new Roo.htmleditor.FilterAttributes({
1401 attrib_black : this.ablack,
1402 attrib_clean : this.aclean,
1403 style_white : this.cwhite,
1404 style_black : this.cblack
1406 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1407 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1413 * Clean up MS wordisms...
1414 * @deprecated - use filter directly
1416 cleanWord : function(node)
1418 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1419 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1426 * @deprecated - use filters
1428 cleanTableWidths : function(node)
1430 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1437 applyBlacklists : function()
1439 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1440 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1442 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1443 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1444 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1448 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1449 if (b.indexOf(tag) > -1) {
1452 this.white.push(tag);
1456 Roo.each(w, function(tag) {
1457 if (b.indexOf(tag) > -1) {
1460 if (this.white.indexOf(tag) > -1) {
1463 this.white.push(tag);
1468 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1469 if (w.indexOf(tag) > -1) {
1472 this.black.push(tag);
1476 Roo.each(b, function(tag) {
1477 if (w.indexOf(tag) > -1) {
1480 if (this.black.indexOf(tag) > -1) {
1483 this.black.push(tag);
1488 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1489 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1493 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1494 if (b.indexOf(tag) > -1) {
1497 this.cwhite.push(tag);
1501 Roo.each(w, function(tag) {
1502 if (b.indexOf(tag) > -1) {
1505 if (this.cwhite.indexOf(tag) > -1) {
1508 this.cwhite.push(tag);
1513 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1514 if (w.indexOf(tag) > -1) {
1517 this.cblack.push(tag);
1521 Roo.each(b, function(tag) {
1522 if (w.indexOf(tag) > -1) {
1525 if (this.cblack.indexOf(tag) > -1) {
1528 this.cblack.push(tag);
1533 setStylesheets : function(stylesheets)
1535 if(typeof(stylesheets) == 'string'){
1536 Roo.get(this.iframe.contentDocument.head).createChild({
1547 Roo.each(stylesheets, function(s) {
1552 Roo.get(_this.iframe.contentDocument.head).createChild({
1564 updateLanguage : function()
1566 if (!this.iframe || !this.iframe.contentDocument) {
1569 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1573 removeStylesheets : function()
1577 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1582 setStyle : function(style)
1584 Roo.get(this.iframe.contentDocument.head).createChild({
1593 // hide stuff that is not compatible
1611 * @cfg {String} fieldClass @hide
1614 * @cfg {String} focusClass @hide
1617 * @cfg {String} autoCreate @hide
1620 * @cfg {String} inputType @hide
1623 * @cfg {String} invalidClass @hide
1626 * @cfg {String} invalidText @hide
1629 * @cfg {String} msgFx @hide
1632 * @cfg {String} validateOnBlur @hide
1636 Roo.HtmlEditorCore.white = [
1637 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1639 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1640 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1641 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1642 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1643 'TABLE', 'UL', 'XMP',
1645 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1648 'DIR', 'MENU', 'OL', 'UL', 'DL',
1654 Roo.HtmlEditorCore.black = [
1655 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1657 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1658 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1659 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1660 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1661 //'FONT' // CLEAN LATER..
1662 'COLGROUP', 'COL' // messy tables.
1666 Roo.HtmlEditorCore.clean = [ // ?? needed???
1667 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1669 Roo.HtmlEditorCore.tag_remove = [
1674 Roo.HtmlEditorCore.ablack = [
1678 Roo.HtmlEditorCore.aclean = [
1679 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1683 Roo.HtmlEditorCore.pwhite= [
1684 'http', 'https', 'mailto'
1687 // white listed style attributes.
1688 Roo.HtmlEditorCore.cwhite= [
1689 // 'text-align', /// default is to allow most things..
1695 // black listed style attributes.
1696 Roo.HtmlEditorCore.cblack= [
1697 // 'font-size' -- this can be set by the project