1 //<script type="text/javascript">
4 * Based Ext JS Library 1.1.1
5 * Copyright(c) 2006-2007, Ext JS, LLC.
11 * @class Roo.HtmlEditorCore
12 * @extends Roo.Component
13 * Provides a the editing component for the HTML editors in Roo. (bootstrap and Roo.form)
15 * any element that has display set to 'none' can cause problems in Safari and Firefox.<br/><br/>
18 Roo.HtmlEditorCore = function(config){
21 Roo.HtmlEditorCore.superclass.constructor.call(this, config);
27 * Fires when the editor is fully initialized (including the iframe)
28 * @param {Roo.HtmlEditorCore} this
33 * Fires when the editor is first receives the focus. Any insertion must wait
34 * until after this event.
35 * @param {Roo.HtmlEditorCore} this
40 * Fires before the textarea is updated with content from the editor iframe. Return false
42 * @param {Roo.HtmlEditorCore} this
43 * @param {String} html
48 * Fires before the iframe editor is updated with content from the textarea. Return false
50 * @param {Roo.HtmlEditorCore} this
51 * @param {String} html
56 * Fires when the textarea is updated with content from the editor iframe.
57 * @param {Roo.HtmlEditorCore} this
58 * @param {String} html
63 * Fires when the iframe editor is updated with content from the textarea.
64 * @param {Roo.HtmlEditorCore} this
65 * @param {String} html
71 * Fires when on any editor (mouse up/down cursor movement etc.) - used for toolbar hooks.
72 * @param {Roo.HtmlEditorCore} this
79 // at this point this.owner is set, so we can start working out the whitelisted / blacklisted elements
81 // defaults : white / black...
82 this.applyBlacklists();
89 Roo.extend(Roo.HtmlEditorCore, Roo.Component, {
93 * @cfg {Roo.form.HtmlEditor|Roo.bootstrap.HtmlEditor} the owner field
99 * @cfg {String} css styling for resizing. (used on bootstrap only)
103 * @cfg {Number} height (in pixels)
107 * @cfg {Number} width (in pixels)
111 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
112 * if you are doing an email editor, this probably needs disabling, it's designed
117 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
121 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
126 * @cfg {String} language default en - language of text (usefull for rtl languages)
132 * @cfg {boolean} allowComments - default false - allow comments in HTML source
133 * - by default they are stripped - if you are editing email you may need this.
135 allowComments: false,
139 // private properties
140 validationEvent : false,
144 sourceEditMode : false,
145 onFocus : Roo.emptyFn,
151 // blacklist + whitelisted elements..
160 * Protected method that will not generally be called directly. It
161 * is called when the editor initializes the iframe with HTML contents. Override this method if you
162 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
164 getDocMarkup : function(){
168 // inherit styels from page...??
169 if (this.stylesheets === false) {
171 Roo.get(document.head).select('style').each(function(node) {
172 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
175 Roo.get(document.head).select('link').each(function(node) {
176 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
179 } else if (!this.stylesheets.length) {
181 st = '<style type="text/css">' +
182 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
185 for (var i in this.stylesheets) {
186 if (typeof(this.stylesheets[i]) != 'string') {
189 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
194 st += '<style type="text/css">' +
195 'IMG { cursor: pointer } ' +
198 st += '<meta name="google" content="notranslate">';
200 var cls = 'notranslate roo-htmleditor-body';
202 if(this.bodyCls.length){
203 cls += ' ' + this.bodyCls;
206 return '<html class="notranslate" translate="no"><head>' + st +
207 //<style type="text/css">' +
208 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
210 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
214 onRender : function(ct, position)
217 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
218 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
221 this.el.dom.style.border = '0 none';
222 this.el.dom.setAttribute('tabIndex', -1);
223 this.el.addClass('x-hidden hide');
227 if(Roo.isIE){ // fix IE 1px bogus margin
228 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
232 this.frameId = Roo.id();
236 cls: 'form-control', // bootstrap..
240 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
243 ifcfg.style = { resize : this.resize };
246 var iframe = this.owner.wrap.createChild(ifcfg, this.el);
249 this.iframe = iframe.dom;
253 this.doc.designMode = 'on';
256 this.doc.write(this.getDocMarkup());
260 var task = { // must defer to wait for browser to be ready
262 //console.log("run task?" + this.doc.readyState);
264 if(this.doc.body || this.doc.readyState == 'complete'){
266 this.doc.designMode="on";
271 Roo.TaskMgr.stop(task);
272 this.initEditor.defer(10, this);
279 Roo.TaskMgr.start(task);
284 onResize : function(w, h)
286 Roo.log('resize: ' +w + ',' + h );
287 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
291 if(typeof w == 'number'){
293 this.iframe.style.width = w + 'px';
295 if(typeof h == 'number'){
297 this.iframe.style.height = h + 'px';
299 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
306 * Toggles the editor between standard and source edit mode.
307 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
309 toggleSourceEdit : function(sourceEditMode){
311 this.sourceEditMode = sourceEditMode === true;
313 if(this.sourceEditMode){
315 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
318 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
319 //this.iframe.className = '';
322 //this.setSize(this.owner.wrap.getSize());
323 //this.fireEvent('editmodechange', this, this.sourceEditMode);
330 * Protected method that will not generally be called directly. If you need/want
331 * custom HTML cleanup, this is the method you should override.
332 * @param {String} html The HTML to be cleaned
333 * return {String} The cleaned HTML
335 cleanHtml : function(html)
339 if(Roo.isSafari){ // strip safari nonsense
340 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
343 if(html == ' '){
350 * HTML Editor -> Textarea
351 * Protected method that will not generally be called directly. Syncs the contents
352 * of the editor iframe with the textarea.
354 syncValue : function()
356 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
357 if(this.initialized){
359 if (this.undoManager) {
360 this.undoManager.addEvent();
364 var bd = (this.doc.body || this.doc.documentElement);
367 var sel = this.win.getSelection();
369 var div = document.createElement('div');
370 div.innerHTML = bd.innerHTML;
371 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
372 if (gtx.length > 0) {
373 var rm = gtx.item(0).parentNode;
374 rm.parentNode.removeChild(rm);
378 if (this.enableBlocks) {
379 new Roo.htmleditor.FilterBlock({ node : div });
382 var html = div.innerHTML;
385 if (this.autoClean) {
387 new Roo.htmleditor.FilterAttributes({
408 attrib_clean : ['href', 'src' ]
411 var tidy = new Roo.htmleditor.TidySerializer({
414 html = tidy.serialize(div);
420 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
421 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
423 html = '<div style="'+m[0]+'">' + html + '</div>';
426 html = this.cleanHtml(html);
427 // fix up the special chars.. normaly like back quotes in word...
428 // however we do not want to do this with chinese..
429 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
431 var cc = match.charCodeAt();
433 // Get the character value, handling surrogate pairs
434 if (match.length == 2) {
435 // It's a surrogate pair, calculate the Unicode code point
436 var high = match.charCodeAt(0) - 0xD800;
437 var low = match.charCodeAt(1) - 0xDC00;
438 cc = (high * 0x400) + low + 0x10000;
440 (cc >= 0x4E00 && cc < 0xA000 ) ||
441 (cc >= 0x3400 && cc < 0x4E00 ) ||
442 (cc >= 0xf900 && cc < 0xfb00 )
447 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
448 return "&#" + cc + ";";
455 if(this.owner.fireEvent('beforesync', this, html) !== false){
456 this.el.dom.value = html;
457 this.owner.fireEvent('sync', this, html);
463 * TEXTAREA -> EDITABLE
464 * Protected method that will not generally be called directly. Pushes the value of the textarea
465 * into the iframe editor.
467 pushValue : function()
469 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
470 if(this.initialized){
471 var v = this.el.dom.value.trim();
474 if(this.owner.fireEvent('beforepush', this, v) !== false){
475 var d = (this.doc.body || this.doc.documentElement);
478 this.el.dom.value = d.innerHTML;
479 this.owner.fireEvent('push', this, v);
481 if (this.autoClean) {
482 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
483 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
485 if (this.enableBlocks) {
486 Roo.htmleditor.Block.initAll(this.doc.body);
489 this.updateLanguage();
491 var lc = this.doc.body.lastChild;
492 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
493 // add an extra line at the end.
494 this.doc.body.appendChild(this.doc.createElement('br'));
502 deferFocus : function(){
503 this.focus.defer(10, this);
508 if(this.win && !this.sourceEditMode){
515 assignDocWin: function()
517 var iframe = this.iframe;
520 this.doc = iframe.contentWindow.document;
521 this.win = iframe.contentWindow;
523 // if (!Roo.get(this.frameId)) {
526 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
527 // this.win = Roo.get(this.frameId).dom.contentWindow;
529 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
533 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
534 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
539 initEditor : function(){
540 //console.log("INIT EDITOR");
545 this.doc.designMode="on";
547 this.doc.write(this.getDocMarkup());
550 var dbody = (this.doc.body || this.doc.documentElement);
551 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
552 // this copies styles from the containing element into thsi one..
553 // not sure why we need all of this..
554 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
556 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
557 //ss['background-attachment'] = 'fixed'; // w3c
558 dbody.bgProperties = 'fixed'; // ie
559 dbody.setAttribute("translate", "no");
561 //Roo.DomHelper.applyStyles(dbody, ss);
562 Roo.EventManager.on(this.doc, {
564 'mouseup': this.onEditorEvent,
565 'dblclick': this.onEditorEvent,
566 'click': this.onEditorEvent,
567 'keyup': this.onEditorEvent,
572 Roo.EventManager.on(this.doc, {
573 'paste': this.onPasteEvent,
577 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
580 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
581 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
583 this.initialized = true;
586 // initialize special key events - enter
587 new Roo.htmleditor.KeyEnter({core : this});
591 this.owner.fireEvent('initialize', this);
594 // this is to prevent a href clicks resulting in a redirect?
596 onPasteEvent : function(e,v)
598 // I think we better assume paste is going to be a dirty load of rubish from word..
600 // even pasting into a 'email version' of this widget will have to clean up that mess.
601 var cd = (e.browserEvent.clipboardData || window.clipboardData);
603 // check what type of paste - if it's an image, then handle it differently.
604 if (cd.files && cd.files.length > 0) {
606 var urlAPI = (window.createObjectURL && window) ||
607 (window.URL && URL.revokeObjectURL && URL) ||
608 (window.webkitURL && webkitURL);
610 var url = urlAPI.createObjectURL( cd.files[0]);
611 this.insertAtCursor('<img src="' + url + '">');
612 var d = (new DOMParser().parseFromString('<img src="' + url + '">', 'text/html')).body;
614 if (this.enableBlocks) {
616 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
617 if (img.closest('figure')) { // assume!! that it's aready
620 var fig = new Roo.htmleditor.BlockFigure({
623 fig.updateElement(img); // replace it..
627 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
630 if (cd.types.indexOf('text/html') < 0 ) {
634 var html = cd.getData('text/html'); // clipboard event
635 if (cd.types.indexOf('text/rtf') > -1) {
636 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
637 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
642 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
643 .map(function(g) { return g.toDataURL(); })
644 .filter(function(g) { return g != 'about:blank'; });
647 html = this.cleanWordChars(html);
649 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
652 var sn = this.getParentElement();
653 // check if d contains a table, and prevent nesting??
654 //Roo.log(d.getElementsByTagName('table'));
656 //Roo.log(sn.closest('table'));
657 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
659 this.insertAtCursor("You can not nest tables");
660 //Roo.log("prevent?"); // fixme -
666 if (images.length > 0) {
667 // replace all v:imagedata - with img.
668 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
669 Roo.each(ar, function(node) {
670 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
671 node.parentNode.removeChild(node);
675 Roo.each(d.getElementsByTagName('img'), function(img, i) {
676 img.setAttribute('src', images[i]);
679 if (this.autoClean) {
680 new Roo.htmleditor.FilterWord({ node : d });
682 new Roo.htmleditor.FilterStyleToTag({ node : d });
683 new Roo.htmleditor.FilterAttributes({
685 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
686 attrib_clean : ['href', 'src' ]
688 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
690 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
691 new Roo.htmleditor.FilterParagraph({ node : d });
692 new Roo.htmleditor.FilterSpan({ node : d });
693 new Roo.htmleditor.FilterLongBr({ node : d });
694 new Roo.htmleditor.FilterComment({ node : d });
698 if (this.enableBlocks) {
700 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
701 if (img.closest('figure')) { // assume!! that it's aready
704 var fig = new Roo.htmleditor.BlockFigure({
707 fig.updateElement(img); // replace it..
713 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
714 if (this.enableBlocks) {
715 Roo.htmleditor.Block.initAll(this.doc.body);
720 this.owner.fireEvent('paste', this);
722 // default behaveiour should be our local cleanup paste? (optional?)
723 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
724 //this.owner.fireEvent('paste', e, v);
727 onDestroy : function(){
733 //for (var i =0; i < this.toolbars.length;i++) {
734 // // fixme - ask toolbars for heights?
735 // this.toolbars[i].onDestroy();
738 //this.wrap.dom.innerHTML = '';
739 //this.wrap.remove();
744 onFirstFocus : function(){
747 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
749 this.activated = true;
752 if(Roo.isGecko){ // prevent silly gecko errors
754 var s = this.win.getSelection();
755 if(!s.focusNode || s.focusNode.nodeType != 3){
756 var r = s.getRangeAt(0);
757 r.selectNodeContents((this.doc.body || this.doc.documentElement));
762 this.execCmd('useCSS', true);
763 this.execCmd('styleWithCSS', false);
766 this.owner.fireEvent('activate', this);
770 adjustFont: function(btn){
771 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
772 //if(Roo.isSafari){ // safari
775 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
776 if(Roo.isSafari){ // safari
777 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
778 v = (v < 10) ? 10 : v;
779 v = (v > 48) ? 48 : v;
780 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
785 v = Math.max(1, v+adjust);
787 this.execCmd('FontSize', v );
790 onEditorEvent : function(e)
794 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
795 return; // we do not handle this.. (undo manager does..)
797 // in theory this detects if the last element is not a br, then we try and do that.
798 // its so clicking in space at bottom triggers adding a br and moving the cursor.
800 e.target.nodeName == 'BODY' &&
801 e.type == "mouseup" &&
802 this.doc.body.lastChild
804 var lc = this.doc.body.lastChild;
805 // gtx-trans is google translate plugin adding crap.
806 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
807 lc = lc.previousSibling;
809 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
810 // if last element is <BR> - then dont do anything.
812 var ns = this.doc.createElement('br');
813 this.doc.body.appendChild(ns);
814 range = this.doc.createRange();
815 range.setStartAfter(ns);
816 range.collapse(true);
817 var sel = this.win.getSelection();
818 sel.removeAllRanges();
825 this.fireEditorEvent(e);
826 // this.updateToolbar();
827 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
830 fireEditorEvent: function(e)
832 this.owner.fireEvent('editorevent', this, e);
835 insertTag : function(tg)
837 // could be a bit smarter... -> wrap the current selected tRoo..
838 if (tg.toLowerCase() == 'span' ||
839 tg.toLowerCase() == 'code' ||
840 tg.toLowerCase() == 'sup' ||
841 tg.toLowerCase() == 'sub'
844 range = this.createRange(this.getSelection());
845 var wrappingNode = this.doc.createElement(tg.toLowerCase());
846 wrappingNode.appendChild(range.extractContents());
847 range.insertNode(wrappingNode);
854 this.execCmd("formatblock", tg);
855 this.undoManager.addEvent();
858 insertText : function(txt)
862 var range = this.createRange();
863 range.deleteContents();
864 //alert(Sender.getAttribute('label'));
866 range.insertNode(this.doc.createTextNode(txt));
867 this.undoManager.addEvent();
873 * Executes a Midas editor command on the editor document and performs necessary focus and
874 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
875 * @param {String} cmd The Midas command
876 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
878 relayCmd : function(cmd, value)
884 case 'justifycenter':
885 // if we are in a cell, then we will adjust the
886 var n = this.getParentElement();
887 var td = n.closest('td');
889 var bl = Roo.htmleditor.Block.factory(td);
890 bl.textAlign = cmd.replace('justify','');
892 this.owner.fireEvent('editorevent', this);
895 this.execCmd('styleWithCSS', true); //
899 // if there is no selection, then we insert, and set the curson inside it..
900 this.execCmd('styleWithCSS', false);
910 this.execCmd(cmd, value);
911 this.owner.fireEvent('editorevent', this);
912 //this.updateToolbar();
913 this.owner.deferFocus();
917 * Executes a Midas editor command directly on the editor document.
918 * For visual commands, you should use {@link #relayCmd} instead.
919 * <b>This should only be called after the editor is initialized.</b>
920 * @param {String} cmd The Midas command
921 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
923 execCmd : function(cmd, value){
924 this.doc.execCommand(cmd, false, value === undefined ? null : value);
931 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
933 * @param {String} text | dom node..
935 insertAtCursor : function(text)
942 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
946 // from jquery ui (MIT licenced)
950 if (win.getSelection && win.getSelection().getRangeAt) {
952 // delete the existing?
954 this.createRange(this.getSelection()).deleteContents();
955 range = win.getSelection().getRangeAt(0);
956 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
957 range.insertNode(node);
958 range = range.cloneRange();
959 range.collapse(false);
961 win.getSelection().removeAllRanges();
962 win.getSelection().addRange(range);
966 } else if (win.document.selection && win.document.selection.createRange) {
967 // no firefox support
968 var txt = typeof(text) == 'string' ? text : text.outerHTML;
969 win.document.selection.createRange().pasteHTML(txt);
972 // no firefox support
973 var txt = typeof(text) == 'string' ? text : text.outerHTML;
974 this.execCmd('InsertHTML', txt);
982 mozKeyPress : function(e){
984 var c = e.getCharCode(), cmd;
987 c = String.fromCharCode(c).toLowerCase();
1001 // this.cleanUpPaste.defer(100, this);
1009 //this.execCmd(cmd);
1010 //this.deferFocus();
1019 fixKeys : function(){ // load time branching for fastest keydown performance
1024 var k = e.getKey(), r;
1027 r = this.doc.selection.createRange();
1030 r.pasteHTML('    ');
1035 /// this is handled by Roo.htmleditor.KeyEnter
1038 r = this.doc.selection.createRange();
1040 var target = r.parentElement();
1041 if(!target || target.tagName.toLowerCase() != 'li'){
1043 r.pasteHTML('<br/>');
1050 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1051 // this.cleanUpPaste.defer(100, this);
1057 }else if(Roo.isOpera){
1063 this.execCmd('InsertHTML','    ');
1067 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1068 // this.cleanUpPaste.defer(100, this);
1073 }else if(Roo.isSafari){
1079 this.execCmd('InsertText','\t');
1083 this.mozKeyPress(e);
1085 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1086 // this.cleanUpPaste.defer(100, this);
1094 getAllAncestors: function()
1096 var p = this.getSelectedNode();
1099 a.push(p); // push blank onto stack..
1100 p = this.getParentElement();
1104 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1108 a.push(this.doc.body);
1112 lastSelNode : false,
1115 getSelection : function()
1117 this.assignDocWin();
1118 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1122 * @param {DomElement} node the node to select
1124 selectNode : function(node, collapse)
1126 var nodeRange = node.ownerDocument.createRange();
1128 nodeRange.selectNode(node);
1130 nodeRange.selectNodeContents(node);
1132 if (collapse === true) {
1133 nodeRange.collapse(true);
1136 var s = this.win.getSelection();
1137 s.removeAllRanges();
1138 s.addRange(nodeRange);
1141 getSelectedNode: function()
1143 // this may only work on Gecko!!!
1145 // should we cache this!!!!
1149 var range = this.createRange(this.getSelection()).cloneRange();
1152 var parent = range.parentElement();
1154 var testRange = range.duplicate();
1155 testRange.moveToElementText(parent);
1156 if (testRange.inRange(range)) {
1159 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1162 parent = parent.parentElement;
1167 // is ancestor a text element.
1168 var ac = range.commonAncestorContainer;
1169 if (ac.nodeType == 3) {
1173 var ar = ac.childNodes;
1176 var other_nodes = [];
1177 var has_other_nodes = false;
1178 for (var i=0;i<ar.length;i++) {
1179 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1182 // fullly contained node.
1184 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1189 // probably selected..
1190 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1191 other_nodes.push(ar[i]);
1195 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1200 has_other_nodes = true;
1202 if (!nodes.length && other_nodes.length) {
1205 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1213 createRange: function(sel)
1215 // this has strange effects when using with
1216 // top toolbar - not sure if it's a great idea.
1217 //this.editor.contentWindow.focus();
1218 if (typeof sel != "undefined") {
1220 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1222 return this.doc.createRange();
1225 return this.doc.createRange();
1228 getParentElement: function()
1231 this.assignDocWin();
1232 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1234 var range = this.createRange(sel);
1237 var p = range.commonAncestorContainer;
1238 while (p.nodeType == 3) { // text node
1249 * Range intersection.. the hard stuff...
1253 * [ -- selected range --- ]
1257 * if end is before start or hits it. fail.
1258 * if start is after end or hits it fail.
1260 * if either hits (but other is outside. - then it's not
1266 // @see http://www.thismuchiknow.co.uk/?p=64.
1267 rangeIntersectsNode : function(range, node)
1269 var nodeRange = node.ownerDocument.createRange();
1271 nodeRange.selectNode(node);
1273 nodeRange.selectNodeContents(node);
1276 var rangeStartRange = range.cloneRange();
1277 rangeStartRange.collapse(true);
1279 var rangeEndRange = range.cloneRange();
1280 rangeEndRange.collapse(false);
1282 var nodeStartRange = nodeRange.cloneRange();
1283 nodeStartRange.collapse(true);
1285 var nodeEndRange = nodeRange.cloneRange();
1286 nodeEndRange.collapse(false);
1288 return rangeStartRange.compareBoundaryPoints(
1289 Range.START_TO_START, nodeEndRange) == -1 &&
1290 rangeEndRange.compareBoundaryPoints(
1291 Range.START_TO_START, nodeStartRange) == 1;
1295 rangeCompareNode : function(range, node)
1297 var nodeRange = node.ownerDocument.createRange();
1299 nodeRange.selectNode(node);
1301 nodeRange.selectNodeContents(node);
1305 range.collapse(true);
1307 nodeRange.collapse(true);
1309 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1310 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1312 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1314 var nodeIsBefore = ss == 1;
1315 var nodeIsAfter = ee == -1;
1317 if (nodeIsBefore && nodeIsAfter) {
1320 if (!nodeIsBefore && nodeIsAfter) {
1321 return 1; //right trailed.
1324 if (nodeIsBefore && !nodeIsAfter) {
1325 return 2; // left trailed.
1331 cleanWordChars : function(input) {// change the chars to hex code
1334 [ 8211, "–" ],
1335 [ 8212, "—" ],
1344 Roo.each(swapCodes, function(sw) {
1345 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1347 output = output.replace(swapper, sw[1]);
1357 cleanUpChild : function (node)
1360 new Roo.htmleditor.FilterComment({node : node});
1361 new Roo.htmleditor.FilterAttributes({
1363 attrib_black : this.ablack,
1364 attrib_clean : this.aclean,
1365 style_white : this.cwhite,
1366 style_black : this.cblack
1368 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1369 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1375 * Clean up MS wordisms...
1376 * @deprecated - use filter directly
1378 cleanWord : function(node)
1380 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1381 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1388 * @deprecated - use filters
1390 cleanTableWidths : function(node)
1392 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1399 applyBlacklists : function()
1401 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1402 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1404 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1405 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1406 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1410 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1411 if (b.indexOf(tag) > -1) {
1414 this.white.push(tag);
1418 Roo.each(w, function(tag) {
1419 if (b.indexOf(tag) > -1) {
1422 if (this.white.indexOf(tag) > -1) {
1425 this.white.push(tag);
1430 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1431 if (w.indexOf(tag) > -1) {
1434 this.black.push(tag);
1438 Roo.each(b, function(tag) {
1439 if (w.indexOf(tag) > -1) {
1442 if (this.black.indexOf(tag) > -1) {
1445 this.black.push(tag);
1450 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1451 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1455 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1456 if (b.indexOf(tag) > -1) {
1459 this.cwhite.push(tag);
1463 Roo.each(w, function(tag) {
1464 if (b.indexOf(tag) > -1) {
1467 if (this.cwhite.indexOf(tag) > -1) {
1470 this.cwhite.push(tag);
1475 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1476 if (w.indexOf(tag) > -1) {
1479 this.cblack.push(tag);
1483 Roo.each(b, function(tag) {
1484 if (w.indexOf(tag) > -1) {
1487 if (this.cblack.indexOf(tag) > -1) {
1490 this.cblack.push(tag);
1495 setStylesheets : function(stylesheets)
1497 if(typeof(stylesheets) == 'string'){
1498 Roo.get(this.iframe.contentDocument.head).createChild({
1509 Roo.each(stylesheets, function(s) {
1514 Roo.get(_this.iframe.contentDocument.head).createChild({
1526 updateLanguage : function()
1528 if (!this.iframe || !this.iframe.contentDocument) {
1531 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1535 removeStylesheets : function()
1539 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1544 setStyle : function(style)
1546 Roo.get(this.iframe.contentDocument.head).createChild({
1555 // hide stuff that is not compatible
1573 * @cfg {String} fieldClass @hide
1576 * @cfg {String} focusClass @hide
1579 * @cfg {String} autoCreate @hide
1582 * @cfg {String} inputType @hide
1585 * @cfg {String} invalidClass @hide
1588 * @cfg {String} invalidText @hide
1591 * @cfg {String} msgFx @hide
1594 * @cfg {String} validateOnBlur @hide
1598 Roo.HtmlEditorCore.white = [
1599 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1601 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1602 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1603 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1604 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1605 'TABLE', 'UL', 'XMP',
1607 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1610 'DIR', 'MENU', 'OL', 'UL', 'DL',
1616 Roo.HtmlEditorCore.black = [
1617 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1619 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1620 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1621 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1622 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1623 //'FONT' // CLEAN LATER..
1624 'COLGROUP', 'COL' // messy tables.
1628 Roo.HtmlEditorCore.clean = [ // ?? needed???
1629 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1631 Roo.HtmlEditorCore.tag_remove = [
1636 Roo.HtmlEditorCore.ablack = [
1640 Roo.HtmlEditorCore.aclean = [
1641 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1645 Roo.HtmlEditorCore.pwhite= [
1646 'http', 'https', 'mailto'
1649 // white listed style attributes.
1650 Roo.HtmlEditorCore.cwhite= [
1651 // 'text-align', /// default is to allow most things..
1657 // black listed style attributes.
1658 Roo.HtmlEditorCore.cblack= [
1659 // 'font-size' -- this can be set by the project