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;
383 Roo.log('sync value');
387 if (this.autoClean) {
389 new Roo.htmleditor.FilterAttributes({
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({
696 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
697 attrib_clean : ['href', 'src' ]
699 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
701 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
702 new Roo.htmleditor.FilterParagraph({ node : d });
703 new Roo.htmleditor.FilterSpan({ node : d });
704 new Roo.htmleditor.FilterLongBr({ node : d });
705 new Roo.htmleditor.FilterComment({ node : d });
709 if (this.enableBlocks) {
711 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
712 if (img.closest('figure')) { // assume!! that it's aready
715 var fig = new Roo.htmleditor.BlockFigure({
718 fig.updateElement(img); // replace it..
724 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
725 if (this.enableBlocks) {
726 Roo.htmleditor.Block.initAll(this.doc.body);
731 this.owner.fireEvent('paste', this);
733 // default behaveiour should be our local cleanup paste? (optional?)
734 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
735 //this.owner.fireEvent('paste', e, v);
738 onDestroy : function(){
744 //for (var i =0; i < this.toolbars.length;i++) {
745 // // fixme - ask toolbars for heights?
746 // this.toolbars[i].onDestroy();
749 //this.wrap.dom.innerHTML = '';
750 //this.wrap.remove();
755 onFirstFocus : function(){
758 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
760 this.activated = true;
763 if(Roo.isGecko){ // prevent silly gecko errors
765 var s = this.win.getSelection();
766 if(!s.focusNode || s.focusNode.nodeType != 3){
767 var r = s.getRangeAt(0);
768 r.selectNodeContents((this.doc.body || this.doc.documentElement));
773 this.execCmd('useCSS', true);
774 this.execCmd('styleWithCSS', false);
777 this.owner.fireEvent('activate', this);
781 adjustFont: function(btn){
782 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
783 //if(Roo.isSafari){ // safari
786 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
787 if(Roo.isSafari){ // safari
788 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
789 v = (v < 10) ? 10 : v;
790 v = (v > 48) ? 48 : v;
791 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
796 v = Math.max(1, v+adjust);
798 this.execCmd('FontSize', v );
801 onEditorEvent : function(e)
805 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
806 return; // we do not handle this.. (undo manager does..)
808 // clicking a 'block'?
810 // in theory this detects if the last element is not a br, then we try and do that.
811 // its so clicking in space at bottom triggers adding a br and moving the cursor.
813 e.target.nodeName == 'BODY' &&
814 e.type == "mouseup" &&
815 this.doc.body.lastChild
817 var lc = this.doc.body.lastChild;
818 // gtx-trans is google translate plugin adding crap.
819 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
820 lc = lc.previousSibling;
822 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
823 // if last element is <BR> - then dont do anything.
825 var ns = this.doc.createElement('br');
826 this.doc.body.appendChild(ns);
827 range = this.doc.createRange();
828 range.setStartAfter(ns);
829 range.collapse(true);
830 var sel = this.win.getSelection();
831 sel.removeAllRanges();
838 this.fireEditorEvent(e);
839 // this.updateToolbar();
840 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
843 fireEditorEvent: function(e)
845 this.owner.fireEvent('editorevent', this, e);
848 insertTag : function(tg)
850 // could be a bit smarter... -> wrap the current selected tRoo..
851 if (tg.toLowerCase() == 'span' ||
852 tg.toLowerCase() == 'code' ||
853 tg.toLowerCase() == 'sup' ||
854 tg.toLowerCase() == 'sub'
857 range = this.createRange(this.getSelection());
858 var wrappingNode = this.doc.createElement(tg.toLowerCase());
859 wrappingNode.appendChild(range.extractContents());
860 range.insertNode(wrappingNode);
867 this.execCmd("formatblock", tg);
868 this.undoManager.addEvent();
871 insertText : function(txt)
875 var range = this.createRange();
876 range.deleteContents();
877 //alert(Sender.getAttribute('label'));
879 range.insertNode(this.doc.createTextNode(txt));
880 this.undoManager.addEvent();
886 * Executes a Midas editor command on the editor document and performs necessary focus and
887 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
888 * @param {String} cmd The Midas command
889 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
891 relayCmd : function(cmd, value)
897 case 'justifycenter':
898 // if we are in a cell, then we will adjust the
899 var n = this.getParentElement();
900 var td = n.closest('td');
902 var bl = Roo.htmleditor.Block.factory(td);
903 bl.textAlign = cmd.replace('justify','');
905 this.owner.fireEvent('editorevent', this);
908 this.execCmd('styleWithCSS', true); //
913 // if there is no selection, then we insert, and set the curson inside it..
914 this.execCmd('styleWithCSS', false);
924 this.execCmd(cmd, value);
925 this.owner.fireEvent('editorevent', this);
926 //this.updateToolbar();
927 this.owner.deferFocus();
931 * Executes a Midas editor command directly on the editor document.
932 * For visual commands, you should use {@link #relayCmd} instead.
933 * <b>This should only be called after the editor is initialized.</b>
934 * @param {String} cmd The Midas command
935 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
937 execCmd : function(cmd, value){
938 this.doc.execCommand(cmd, false, value === undefined ? null : value);
945 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
947 * @param {String} text | dom node..
949 insertAtCursor : function(text)
956 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
960 // from jquery ui (MIT licenced)
964 if (win.getSelection && win.getSelection().getRangeAt) {
966 // delete the existing?
968 this.createRange(this.getSelection()).deleteContents();
969 range = win.getSelection().getRangeAt(0);
970 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
971 range.insertNode(node);
972 range = range.cloneRange();
973 range.collapse(false);
975 win.getSelection().removeAllRanges();
976 win.getSelection().addRange(range);
980 } else if (win.document.selection && win.document.selection.createRange) {
981 // no firefox support
982 var txt = typeof(text) == 'string' ? text : text.outerHTML;
983 win.document.selection.createRange().pasteHTML(txt);
986 // no firefox support
987 var txt = typeof(text) == 'string' ? text : text.outerHTML;
988 this.execCmd('InsertHTML', txt);
996 mozKeyPress : function(e){
998 var c = e.getCharCode(), cmd;
1001 c = String.fromCharCode(c).toLowerCase();
1015 // this.cleanUpPaste.defer(100, this);
1023 //this.execCmd(cmd);
1024 //this.deferFocus();
1033 fixKeys : function(){ // load time branching for fastest keydown performance
1038 var k = e.getKey(), r;
1041 r = this.doc.selection.createRange();
1044 r.pasteHTML('    ');
1049 /// this is handled by Roo.htmleditor.KeyEnter
1052 r = this.doc.selection.createRange();
1054 var target = r.parentElement();
1055 if(!target || target.tagName.toLowerCase() != 'li'){
1057 r.pasteHTML('<br/>');
1064 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1065 // this.cleanUpPaste.defer(100, this);
1071 }else if(Roo.isOpera){
1077 this.execCmd('InsertHTML','    ');
1081 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1082 // this.cleanUpPaste.defer(100, this);
1087 }else if(Roo.isSafari){
1093 this.execCmd('InsertText','\t');
1097 this.mozKeyPress(e);
1099 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1100 // this.cleanUpPaste.defer(100, this);
1108 getAllAncestors: function()
1110 var p = this.getSelectedNode();
1113 a.push(p); // push blank onto stack..
1114 p = this.getParentElement();
1118 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1122 a.push(this.doc.body);
1126 lastSelNode : false,
1129 getSelection : function()
1131 this.assignDocWin();
1132 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1136 * @param {DomElement} node the node to select
1138 selectNode : function(node, collapse)
1140 var nodeRange = node.ownerDocument.createRange();
1142 nodeRange.selectNode(node);
1144 nodeRange.selectNodeContents(node);
1146 if (collapse === true) {
1147 nodeRange.collapse(true);
1150 var s = this.win.getSelection();
1151 s.removeAllRanges();
1152 s.addRange(nodeRange);
1155 getSelectedNode: function()
1157 // this may only work on Gecko!!!
1159 // should we cache this!!!!
1163 var range = this.createRange(this.getSelection()).cloneRange();
1166 var parent = range.parentElement();
1168 var testRange = range.duplicate();
1169 testRange.moveToElementText(parent);
1170 if (testRange.inRange(range)) {
1173 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1176 parent = parent.parentElement;
1181 // is ancestor a text element.
1182 var ac = range.commonAncestorContainer;
1183 if (ac.nodeType == 3) {
1187 var ar = ac.childNodes;
1190 var other_nodes = [];
1191 var has_other_nodes = false;
1192 for (var i=0;i<ar.length;i++) {
1193 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1196 // fullly contained node.
1198 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1203 // probably selected..
1204 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1205 other_nodes.push(ar[i]);
1209 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1214 has_other_nodes = true;
1216 if (!nodes.length && other_nodes.length) {
1219 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1227 createRange: function(sel)
1229 // this has strange effects when using with
1230 // top toolbar - not sure if it's a great idea.
1231 //this.editor.contentWindow.focus();
1232 if (typeof sel != "undefined") {
1234 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1236 return this.doc.createRange();
1239 return this.doc.createRange();
1242 getParentElement: function()
1245 this.assignDocWin();
1246 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1248 var range = this.createRange(sel);
1251 var p = range.commonAncestorContainer;
1252 while (p.nodeType == 3) { // text node
1263 * Range intersection.. the hard stuff...
1267 * [ -- selected range --- ]
1271 * if end is before start or hits it. fail.
1272 * if start is after end or hits it fail.
1274 * if either hits (but other is outside. - then it's not
1280 // @see http://www.thismuchiknow.co.uk/?p=64.
1281 rangeIntersectsNode : function(range, node)
1283 var nodeRange = node.ownerDocument.createRange();
1285 nodeRange.selectNode(node);
1287 nodeRange.selectNodeContents(node);
1290 var rangeStartRange = range.cloneRange();
1291 rangeStartRange.collapse(true);
1293 var rangeEndRange = range.cloneRange();
1294 rangeEndRange.collapse(false);
1296 var nodeStartRange = nodeRange.cloneRange();
1297 nodeStartRange.collapse(true);
1299 var nodeEndRange = nodeRange.cloneRange();
1300 nodeEndRange.collapse(false);
1302 return rangeStartRange.compareBoundaryPoints(
1303 Range.START_TO_START, nodeEndRange) == -1 &&
1304 rangeEndRange.compareBoundaryPoints(
1305 Range.START_TO_START, nodeStartRange) == 1;
1309 rangeCompareNode : function(range, node)
1311 var nodeRange = node.ownerDocument.createRange();
1313 nodeRange.selectNode(node);
1315 nodeRange.selectNodeContents(node);
1319 range.collapse(true);
1321 nodeRange.collapse(true);
1323 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1324 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1326 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1328 var nodeIsBefore = ss == 1;
1329 var nodeIsAfter = ee == -1;
1331 if (nodeIsBefore && nodeIsAfter) {
1334 if (!nodeIsBefore && nodeIsAfter) {
1335 return 1; //right trailed.
1338 if (nodeIsBefore && !nodeIsAfter) {
1339 return 2; // left trailed.
1345 cleanWordChars : function(input) {// change the chars to hex code
1348 [ 8211, "–" ],
1349 [ 8212, "—" ],
1358 Roo.each(swapCodes, function(sw) {
1359 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1361 output = output.replace(swapper, sw[1]);
1371 cleanUpChild : function (node)
1374 new Roo.htmleditor.FilterComment({node : node});
1375 new Roo.htmleditor.FilterAttributes({
1377 attrib_black : this.ablack,
1378 attrib_clean : this.aclean,
1379 style_white : this.cwhite,
1380 style_black : this.cblack
1382 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1383 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1389 * Clean up MS wordisms...
1390 * @deprecated - use filter directly
1392 cleanWord : function(node)
1394 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1395 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1402 * @deprecated - use filters
1404 cleanTableWidths : function(node)
1406 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1413 applyBlacklists : function()
1415 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1416 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1418 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1419 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1420 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1424 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1425 if (b.indexOf(tag) > -1) {
1428 this.white.push(tag);
1432 Roo.each(w, function(tag) {
1433 if (b.indexOf(tag) > -1) {
1436 if (this.white.indexOf(tag) > -1) {
1439 this.white.push(tag);
1444 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1445 if (w.indexOf(tag) > -1) {
1448 this.black.push(tag);
1452 Roo.each(b, function(tag) {
1453 if (w.indexOf(tag) > -1) {
1456 if (this.black.indexOf(tag) > -1) {
1459 this.black.push(tag);
1464 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1465 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1469 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1470 if (b.indexOf(tag) > -1) {
1473 this.cwhite.push(tag);
1477 Roo.each(w, function(tag) {
1478 if (b.indexOf(tag) > -1) {
1481 if (this.cwhite.indexOf(tag) > -1) {
1484 this.cwhite.push(tag);
1489 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1490 if (w.indexOf(tag) > -1) {
1493 this.cblack.push(tag);
1497 Roo.each(b, function(tag) {
1498 if (w.indexOf(tag) > -1) {
1501 if (this.cblack.indexOf(tag) > -1) {
1504 this.cblack.push(tag);
1509 setStylesheets : function(stylesheets)
1511 if(typeof(stylesheets) == 'string'){
1512 Roo.get(this.iframe.contentDocument.head).createChild({
1523 Roo.each(stylesheets, function(s) {
1528 Roo.get(_this.iframe.contentDocument.head).createChild({
1540 updateLanguage : function()
1542 if (!this.iframe || !this.iframe.contentDocument) {
1545 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1549 removeStylesheets : function()
1553 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1558 setStyle : function(style)
1560 Roo.get(this.iframe.contentDocument.head).createChild({
1569 // hide stuff that is not compatible
1587 * @cfg {String} fieldClass @hide
1590 * @cfg {String} focusClass @hide
1593 * @cfg {String} autoCreate @hide
1596 * @cfg {String} inputType @hide
1599 * @cfg {String} invalidClass @hide
1602 * @cfg {String} invalidText @hide
1605 * @cfg {String} msgFx @hide
1608 * @cfg {String} validateOnBlur @hide
1612 Roo.HtmlEditorCore.white = [
1613 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1615 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1616 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1617 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1618 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1619 'TABLE', 'UL', 'XMP',
1621 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1624 'DIR', 'MENU', 'OL', 'UL', 'DL',
1630 Roo.HtmlEditorCore.black = [
1631 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1633 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1634 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1635 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1636 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1637 //'FONT' // CLEAN LATER..
1638 'COLGROUP', 'COL' // messy tables.
1642 Roo.HtmlEditorCore.clean = [ // ?? needed???
1643 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1645 Roo.HtmlEditorCore.tag_remove = [
1650 Roo.HtmlEditorCore.ablack = [
1654 Roo.HtmlEditorCore.aclean = [
1655 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1659 Roo.HtmlEditorCore.pwhite= [
1660 'http', 'https', 'mailto'
1663 // white listed style attributes.
1664 Roo.HtmlEditorCore.cwhite= [
1665 // 'text-align', /// default is to allow most things..
1671 // black listed style attributes.
1672 Roo.HtmlEditorCore.cblack= [
1673 // 'font-size' -- this can be set by the project