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.FilterHashLink({node : d});
726 new Roo.htmleditor.FilterSpan({ node : d });
727 new Roo.htmleditor.FilterLongBr({ node : d });
728 new Roo.htmleditor.FilterComment({ node : d });
732 if (this.enableBlocks) {
734 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
735 if (img.closest('figure')) { // assume!! that it's aready
738 var fig = new Roo.htmleditor.BlockFigure({
741 fig.updateElement(img); // replace it..
747 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
748 if (this.enableBlocks) {
749 Roo.htmleditor.Block.initAll(this.doc.body);
754 this.owner.fireEvent('paste', this);
756 // default behaveiour should be our local cleanup paste? (optional?)
757 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
758 //this.owner.fireEvent('paste', e, v);
761 onDestroy : function(){
767 //for (var i =0; i < this.toolbars.length;i++) {
768 // // fixme - ask toolbars for heights?
769 // this.toolbars[i].onDestroy();
772 //this.wrap.dom.innerHTML = '';
773 //this.wrap.remove();
778 onFirstFocus : function(){
781 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
783 this.activated = true;
786 if(Roo.isGecko){ // prevent silly gecko errors
788 var s = this.win.getSelection();
789 if(!s.focusNode || s.focusNode.nodeType != 3){
790 var r = s.getRangeAt(0);
791 r.selectNodeContents((this.doc.body || this.doc.documentElement));
796 this.execCmd('useCSS', true);
797 this.execCmd('styleWithCSS', false);
800 this.owner.fireEvent('activate', this);
804 adjustFont: function(btn){
805 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
806 //if(Roo.isSafari){ // safari
809 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
810 if(Roo.isSafari){ // safari
811 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
812 v = (v < 10) ? 10 : v;
813 v = (v > 48) ? 48 : v;
814 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
819 v = Math.max(1, v+adjust);
821 this.execCmd('FontSize', v );
824 onEditorEvent : function(e)
828 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
829 return; // we do not handle this.. (undo manager does..)
831 // clicking a 'block'?
833 // in theory this detects if the last element is not a br, then we try and do that.
834 // its so clicking in space at bottom triggers adding a br and moving the cursor.
836 e.target.nodeName == 'BODY' &&
837 e.type == "mouseup" &&
838 this.doc.body.lastChild
840 var lc = this.doc.body.lastChild;
841 // gtx-trans is google translate plugin adding crap.
842 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
843 lc = lc.previousSibling;
845 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
846 // if last element is <BR> - then dont do anything.
848 var ns = this.doc.createElement('br');
849 this.doc.body.appendChild(ns);
850 range = this.doc.createRange();
851 range.setStartAfter(ns);
852 range.collapse(true);
853 var sel = this.win.getSelection();
854 sel.removeAllRanges();
861 this.fireEditorEvent(e);
862 // this.updateToolbar();
863 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
866 fireEditorEvent: function(e)
868 this.owner.fireEvent('editorevent', this, e);
871 insertTag : function(tg)
873 // could be a bit smarter... -> wrap the current selected tRoo..
874 if (tg.toLowerCase() == 'span' ||
875 tg.toLowerCase() == 'code' ||
876 tg.toLowerCase() == 'sup' ||
877 tg.toLowerCase() == 'sub'
880 range = this.createRange(this.getSelection());
881 var wrappingNode = this.doc.createElement(tg.toLowerCase());
882 wrappingNode.appendChild(range.extractContents());
883 range.insertNode(wrappingNode);
890 this.execCmd("formatblock", tg);
891 this.undoManager.addEvent();
894 insertText : function(txt)
898 var range = this.createRange();
899 range.deleteContents();
900 //alert(Sender.getAttribute('label'));
902 range.insertNode(this.doc.createTextNode(txt));
903 this.undoManager.addEvent();
909 * Executes a Midas editor command on the editor document and performs necessary focus and
910 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
911 * @param {String} cmd The Midas command
912 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
914 relayCmd : function(cmd, value)
920 case 'justifycenter':
921 // if we are in a cell, then we will adjust the
922 var n = this.getParentElement();
923 var td = n.closest('td');
925 var bl = Roo.htmleditor.Block.factory(td);
926 bl.textAlign = cmd.replace('justify','');
928 this.owner.fireEvent('editorevent', this);
931 this.execCmd('styleWithCSS', true); //
936 // if there is no selection, then we insert, and set the curson inside it..
937 this.execCmd('styleWithCSS', false);
947 this.execCmd(cmd, value);
948 this.owner.fireEvent('editorevent', this);
949 //this.updateToolbar();
950 this.owner.deferFocus();
954 * Executes a Midas editor command directly on the editor document.
955 * For visual commands, you should use {@link #relayCmd} instead.
956 * <b>This should only be called after the editor is initialized.</b>
957 * @param {String} cmd The Midas command
958 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
960 execCmd : function(cmd, value){
961 this.doc.execCommand(cmd, false, value === undefined ? null : value);
968 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
970 * @param {String} text | dom node..
972 insertAtCursor : function(text)
979 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
983 // from jquery ui (MIT licenced)
987 if (win.getSelection && win.getSelection().getRangeAt) {
989 // delete the existing?
991 this.createRange(this.getSelection()).deleteContents();
992 range = win.getSelection().getRangeAt(0);
993 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
994 range.insertNode(node);
995 range = range.cloneRange();
996 range.collapse(false);
998 win.getSelection().removeAllRanges();
999 win.getSelection().addRange(range);
1003 } else if (win.document.selection && win.document.selection.createRange) {
1004 // no firefox support
1005 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1006 win.document.selection.createRange().pasteHTML(txt);
1009 // no firefox support
1010 var txt = typeof(text) == 'string' ? text : text.outerHTML;
1011 this.execCmd('InsertHTML', txt);
1019 mozKeyPress : function(e){
1021 var c = e.getCharCode(), cmd;
1024 c = String.fromCharCode(c).toLowerCase();
1038 // this.cleanUpPaste.defer(100, this);
1046 //this.execCmd(cmd);
1047 //this.deferFocus();
1056 fixKeys : function(){ // load time branching for fastest keydown performance
1061 var k = e.getKey(), r;
1064 r = this.doc.selection.createRange();
1067 r.pasteHTML('    ');
1072 /// this is handled by Roo.htmleditor.KeyEnter
1075 r = this.doc.selection.createRange();
1077 var target = r.parentElement();
1078 if(!target || target.tagName.toLowerCase() != 'li'){
1080 r.pasteHTML('<br/>');
1087 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1088 // this.cleanUpPaste.defer(100, this);
1094 }else if(Roo.isOpera){
1100 this.execCmd('InsertHTML','    ');
1104 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1105 // this.cleanUpPaste.defer(100, this);
1110 }else if(Roo.isSafari){
1116 this.execCmd('InsertText','\t');
1120 this.mozKeyPress(e);
1122 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1123 // this.cleanUpPaste.defer(100, this);
1131 getAllAncestors: function()
1133 var p = this.getSelectedNode();
1136 a.push(p); // push blank onto stack..
1137 p = this.getParentElement();
1141 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1145 a.push(this.doc.body);
1149 lastSelNode : false,
1152 getSelection : function()
1154 this.assignDocWin();
1155 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1159 * @param {DomElement} node the node to select
1161 selectNode : function(node, collapse)
1163 var nodeRange = node.ownerDocument.createRange();
1165 nodeRange.selectNode(node);
1167 nodeRange.selectNodeContents(node);
1169 if (collapse === true) {
1170 nodeRange.collapse(true);
1173 var s = this.win.getSelection();
1174 s.removeAllRanges();
1175 s.addRange(nodeRange);
1178 getSelectedNode: function()
1180 // this may only work on Gecko!!!
1182 // should we cache this!!!!
1186 var range = this.createRange(this.getSelection()).cloneRange();
1189 var parent = range.parentElement();
1191 var testRange = range.duplicate();
1192 testRange.moveToElementText(parent);
1193 if (testRange.inRange(range)) {
1196 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1199 parent = parent.parentElement;
1204 // is ancestor a text element.
1205 var ac = range.commonAncestorContainer;
1206 if (ac.nodeType == 3) {
1210 var ar = ac.childNodes;
1213 var other_nodes = [];
1214 var has_other_nodes = false;
1215 for (var i=0;i<ar.length;i++) {
1216 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1219 // fullly contained node.
1221 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1226 // probably selected..
1227 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1228 other_nodes.push(ar[i]);
1232 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1237 has_other_nodes = true;
1239 if (!nodes.length && other_nodes.length) {
1242 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1250 createRange: function(sel)
1252 // this has strange effects when using with
1253 // top toolbar - not sure if it's a great idea.
1254 //this.editor.contentWindow.focus();
1255 if (typeof sel != "undefined") {
1257 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1259 return this.doc.createRange();
1262 return this.doc.createRange();
1265 getParentElement: function()
1268 this.assignDocWin();
1269 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1271 var range = this.createRange(sel);
1274 var p = range.commonAncestorContainer;
1275 while (p.nodeType == 3) { // text node
1286 * Range intersection.. the hard stuff...
1290 * [ -- selected range --- ]
1294 * if end is before start or hits it. fail.
1295 * if start is after end or hits it fail.
1297 * if either hits (but other is outside. - then it's not
1303 // @see http://www.thismuchiknow.co.uk/?p=64.
1304 rangeIntersectsNode : function(range, node)
1306 var nodeRange = node.ownerDocument.createRange();
1308 nodeRange.selectNode(node);
1310 nodeRange.selectNodeContents(node);
1313 var rangeStartRange = range.cloneRange();
1314 rangeStartRange.collapse(true);
1316 var rangeEndRange = range.cloneRange();
1317 rangeEndRange.collapse(false);
1319 var nodeStartRange = nodeRange.cloneRange();
1320 nodeStartRange.collapse(true);
1322 var nodeEndRange = nodeRange.cloneRange();
1323 nodeEndRange.collapse(false);
1325 return rangeStartRange.compareBoundaryPoints(
1326 Range.START_TO_START, nodeEndRange) == -1 &&
1327 rangeEndRange.compareBoundaryPoints(
1328 Range.START_TO_START, nodeStartRange) == 1;
1332 rangeCompareNode : function(range, node)
1334 var nodeRange = node.ownerDocument.createRange();
1336 nodeRange.selectNode(node);
1338 nodeRange.selectNodeContents(node);
1342 range.collapse(true);
1344 nodeRange.collapse(true);
1346 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1347 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1349 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1351 var nodeIsBefore = ss == 1;
1352 var nodeIsAfter = ee == -1;
1354 if (nodeIsBefore && nodeIsAfter) {
1357 if (!nodeIsBefore && nodeIsAfter) {
1358 return 1; //right trailed.
1361 if (nodeIsBefore && !nodeIsAfter) {
1362 return 2; // left trailed.
1368 cleanWordChars : function(input) {// change the chars to hex code
1371 [ 8211, "–" ],
1372 [ 8212, "—" ],
1381 Roo.each(swapCodes, function(sw) {
1382 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1384 output = output.replace(swapper, sw[1]);
1394 cleanUpChild : function (node)
1397 new Roo.htmleditor.FilterComment({node : node});
1398 new Roo.htmleditor.FilterAttributes({
1400 attrib_black : this.ablack,
1401 attrib_clean : this.aclean,
1402 style_white : this.cwhite,
1403 style_black : this.cblack
1405 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1406 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1412 * Clean up MS wordisms...
1413 * @deprecated - use filter directly
1415 cleanWord : function(node)
1417 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1418 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1425 * @deprecated - use filters
1427 cleanTableWidths : function(node)
1429 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1436 applyBlacklists : function()
1438 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1439 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1441 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1442 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1443 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1447 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1448 if (b.indexOf(tag) > -1) {
1451 this.white.push(tag);
1455 Roo.each(w, function(tag) {
1456 if (b.indexOf(tag) > -1) {
1459 if (this.white.indexOf(tag) > -1) {
1462 this.white.push(tag);
1467 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1468 if (w.indexOf(tag) > -1) {
1471 this.black.push(tag);
1475 Roo.each(b, function(tag) {
1476 if (w.indexOf(tag) > -1) {
1479 if (this.black.indexOf(tag) > -1) {
1482 this.black.push(tag);
1487 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1488 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1492 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1493 if (b.indexOf(tag) > -1) {
1496 this.cwhite.push(tag);
1500 Roo.each(w, function(tag) {
1501 if (b.indexOf(tag) > -1) {
1504 if (this.cwhite.indexOf(tag) > -1) {
1507 this.cwhite.push(tag);
1512 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1513 if (w.indexOf(tag) > -1) {
1516 this.cblack.push(tag);
1520 Roo.each(b, function(tag) {
1521 if (w.indexOf(tag) > -1) {
1524 if (this.cblack.indexOf(tag) > -1) {
1527 this.cblack.push(tag);
1532 setStylesheets : function(stylesheets)
1534 if(typeof(stylesheets) == 'string'){
1535 Roo.get(this.iframe.contentDocument.head).createChild({
1546 Roo.each(stylesheets, function(s) {
1551 Roo.get(_this.iframe.contentDocument.head).createChild({
1563 updateLanguage : function()
1565 if (!this.iframe || !this.iframe.contentDocument) {
1568 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1572 removeStylesheets : function()
1576 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1581 setStyle : function(style)
1583 Roo.get(this.iframe.contentDocument.head).createChild({
1592 // hide stuff that is not compatible
1610 * @cfg {String} fieldClass @hide
1613 * @cfg {String} focusClass @hide
1616 * @cfg {String} autoCreate @hide
1619 * @cfg {String} inputType @hide
1622 * @cfg {String} invalidClass @hide
1625 * @cfg {String} invalidText @hide
1628 * @cfg {String} msgFx @hide
1631 * @cfg {String} validateOnBlur @hide
1635 Roo.HtmlEditorCore.white = [
1636 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1638 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1639 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1640 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1641 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1642 'TABLE', 'UL', 'XMP',
1644 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1647 'DIR', 'MENU', 'OL', 'UL', 'DL',
1653 Roo.HtmlEditorCore.black = [
1654 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1656 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1657 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1658 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1659 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1660 //'FONT' // CLEAN LATER..
1661 'COLGROUP', 'COL' // messy tables.
1665 Roo.HtmlEditorCore.clean = [ // ?? needed???
1666 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1668 Roo.HtmlEditorCore.tag_remove = [
1673 Roo.HtmlEditorCore.ablack = [
1677 Roo.HtmlEditorCore.aclean = [
1678 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1682 Roo.HtmlEditorCore.pwhite= [
1683 'http', 'https', 'mailto'
1686 // white listed style attributes.
1687 Roo.HtmlEditorCore.cwhite= [
1688 // 'text-align', /// default is to allow most things..
1694 // black listed style attributes.
1695 Roo.HtmlEditorCore.cblack= [
1696 // 'font-size' -- this can be set by the project