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} resizable 's' or 'se' or 'e' - wrapps the element in a
104 * @cfg {Number} height (in pixels)
108 * @cfg {Number} width (in pixels)
112 * @cfg {boolean} autoClean - default true - loading and saving will remove quite a bit of formating,
113 * if you are doing an email editor, this probably needs disabling, it's designed
118 * @cfg {boolean} enableBlocks - default true - if the block editor (table and figure should be enabled)
122 * @cfg {Array} stylesheets url of stylesheets. set to [] to disable stylesheets.
127 * @cfg {String} language default en - language of text (usefull for rtl languages)
133 * @cfg {boolean} allowComments - default false - allow comments in HTML source
134 * - by default they are stripped - if you are editing email you may need this.
136 allowComments: false,
140 // private properties
141 validationEvent : false,
145 sourceEditMode : false,
146 onFocus : Roo.emptyFn,
152 // blacklist + whitelisted elements..
161 * Protected method that will not generally be called directly. It
162 * is called when the editor initializes the iframe with HTML contents. Override this method if you
163 * want to change the initialization markup of the iframe (e.g. to add stylesheets).
165 getDocMarkup : function(){
169 // inherit styels from page...??
170 if (this.stylesheets === false) {
172 Roo.get(document.head).select('style').each(function(node) {
173 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
176 Roo.get(document.head).select('link').each(function(node) {
177 st += node.dom.outerHTML || new XMLSerializer().serializeToString(node.dom);
180 } else if (!this.stylesheets.length) {
182 st = '<style type="text/css">' +
183 'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
186 for (var i in this.stylesheets) {
187 if (typeof(this.stylesheets[i]) != 'string') {
190 st += '<link rel="stylesheet" href="' + this.stylesheets[i] +'" type="text/css">';
195 st += '<style type="text/css">' +
196 'IMG { cursor: pointer } ' +
199 st += '<meta name="google" content="notranslate">';
201 var cls = 'notranslate roo-htmleditor-body';
203 if(this.bodyCls.length){
204 cls += ' ' + this.bodyCls;
207 return '<html class="notranslate" translate="no"><head>' + st +
208 //<style type="text/css">' +
209 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
211 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
215 onRender : function(ct, position)
218 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
219 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
222 this.el.dom.style.border = '0 none';
223 this.el.dom.setAttribute('tabIndex', -1);
224 this.el.addClass('x-hidden hide');
228 if(Roo.isIE){ // fix IE 1px bogus margin
229 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
233 this.frameId = Roo.id();
237 var iframe = this.owner.wrap.createChild({
239 cls: 'form-control', // bootstrap..
243 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
248 this.iframe = iframe.dom;
252 this.doc.designMode = 'on';
255 this.doc.write(this.getDocMarkup());
259 var task = { // must defer to wait for browser to be ready
261 //console.log("run task?" + this.doc.readyState);
263 if(this.doc.body || this.doc.readyState == 'complete'){
265 this.doc.designMode="on";
270 Roo.TaskMgr.stop(task);
271 this.initEditor.defer(10, this);
278 Roo.TaskMgr.start(task);
283 onResize : function(w, h)
285 Roo.log('resize: ' +w + ',' + h );
286 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
290 if(typeof w == 'number'){
292 this.iframe.style.width = w + 'px';
294 if(typeof h == 'number'){
296 this.iframe.style.height = h + 'px';
298 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
305 * Toggles the editor between standard and source edit mode.
306 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
308 toggleSourceEdit : function(sourceEditMode){
310 this.sourceEditMode = sourceEditMode === true;
312 if(this.sourceEditMode){
314 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
317 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
318 //this.iframe.className = '';
321 //this.setSize(this.owner.wrap.getSize());
322 //this.fireEvent('editmodechange', this, this.sourceEditMode);
329 * Protected method that will not generally be called directly. If you need/want
330 * custom HTML cleanup, this is the method you should override.
331 * @param {String} html The HTML to be cleaned
332 * return {String} The cleaned HTML
334 cleanHtml : function(html)
338 if(Roo.isSafari){ // strip safari nonsense
339 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
342 if(html == ' '){
349 * HTML Editor -> Textarea
350 * Protected method that will not generally be called directly. Syncs the contents
351 * of the editor iframe with the textarea.
353 syncValue : function()
355 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
356 if(this.initialized){
358 if (this.undoManager) {
359 this.undoManager.addEvent();
363 var bd = (this.doc.body || this.doc.documentElement);
366 var sel = this.win.getSelection();
368 var div = document.createElement('div');
369 div.innerHTML = bd.innerHTML;
370 var gtx = div.getElementsByClassName('gtx-trans-icon'); // google translate - really annoying and difficult to get rid of.
371 if (gtx.length > 0) {
372 var rm = gtx.item(0).parentNode;
373 rm.parentNode.removeChild(rm);
377 if (this.enableBlocks) {
378 new Roo.htmleditor.FilterBlock({ node : div });
381 var html = div.innerHTML;
384 if (this.autoClean) {
386 new Roo.htmleditor.FilterAttributes({
407 attrib_clean : ['href', 'src' ]
410 var tidy = new Roo.htmleditor.TidySerializer({
413 html = tidy.serialize(div);
419 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
420 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
422 html = '<div style="'+m[0]+'">' + html + '</div>';
425 html = this.cleanHtml(html);
426 // fix up the special chars.. normaly like back quotes in word...
427 // however we do not want to do this with chinese..
428 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
430 var cc = match.charCodeAt();
432 // Get the character value, handling surrogate pairs
433 if (match.length == 2) {
434 // It's a surrogate pair, calculate the Unicode code point
435 var high = match.charCodeAt(0) - 0xD800;
436 var low = match.charCodeAt(1) - 0xDC00;
437 cc = (high * 0x400) + low + 0x10000;
439 (cc >= 0x4E00 && cc < 0xA000 ) ||
440 (cc >= 0x3400 && cc < 0x4E00 ) ||
441 (cc >= 0xf900 && cc < 0xfb00 )
446 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
447 return "&#" + cc + ";";
454 if(this.owner.fireEvent('beforesync', this, html) !== false){
455 this.el.dom.value = html;
456 this.owner.fireEvent('sync', this, html);
462 * TEXTAREA -> EDITABLE
463 * Protected method that will not generally be called directly. Pushes the value of the textarea
464 * into the iframe editor.
466 pushValue : function()
468 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
469 if(this.initialized){
470 var v = this.el.dom.value.trim();
473 if(this.owner.fireEvent('beforepush', this, v) !== false){
474 var d = (this.doc.body || this.doc.documentElement);
477 this.el.dom.value = d.innerHTML;
478 this.owner.fireEvent('push', this, v);
480 if (this.autoClean) {
481 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
482 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
484 if (this.enableBlocks) {
485 Roo.htmleditor.Block.initAll(this.doc.body);
488 this.updateLanguage();
490 var lc = this.doc.body.lastChild;
491 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
492 // add an extra line at the end.
493 this.doc.body.appendChild(this.doc.createElement('br'));
501 deferFocus : function(){
502 this.focus.defer(10, this);
507 if(this.win && !this.sourceEditMode){
514 assignDocWin: function()
516 var iframe = this.iframe;
519 this.doc = iframe.contentWindow.document;
520 this.win = iframe.contentWindow;
522 // if (!Roo.get(this.frameId)) {
525 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
526 // this.win = Roo.get(this.frameId).dom.contentWindow;
528 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
532 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
533 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
538 initEditor : function(){
539 //console.log("INIT EDITOR");
544 this.doc.designMode="on";
546 this.doc.write(this.getDocMarkup());
549 var dbody = (this.doc.body || this.doc.documentElement);
550 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
551 // this copies styles from the containing element into thsi one..
552 // not sure why we need all of this..
553 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
555 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
556 //ss['background-attachment'] = 'fixed'; // w3c
557 dbody.bgProperties = 'fixed'; // ie
558 dbody.setAttribute("translate", "no");
560 //Roo.DomHelper.applyStyles(dbody, ss);
561 Roo.EventManager.on(this.doc, {
563 'mouseup': this.onEditorEvent,
564 'dblclick': this.onEditorEvent,
565 'click': this.onEditorEvent,
566 'keyup': this.onEditorEvent,
571 Roo.EventManager.on(this.doc, {
572 'paste': this.onPasteEvent,
576 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
579 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
580 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
582 this.initialized = true;
585 // initialize special key events - enter
586 new Roo.htmleditor.KeyEnter({core : this});
590 this.owner.fireEvent('initialize', this);
593 // this is to prevent a href clicks resulting in a redirect?
595 onPasteEvent : function(e,v)
597 // I think we better assume paste is going to be a dirty load of rubish from word..
599 // even pasting into a 'email version' of this widget will have to clean up that mess.
600 var cd = (e.browserEvent.clipboardData || window.clipboardData);
602 // check what type of paste - if it's an image, then handle it differently.
603 if (cd.files && cd.files.length > 0) {
605 var urlAPI = (window.createObjectURL && window) ||
606 (window.URL && URL.revokeObjectURL && URL) ||
607 (window.webkitURL && webkitURL);
609 var url = urlAPI.createObjectURL( cd.files[0]);
610 this.insertAtCursor('<img src=" + url + ">');
613 if (cd.types.indexOf('text/html') < 0 ) {
617 var html = cd.getData('text/html'); // clipboard event
618 if (cd.types.indexOf('text/rtf') > -1) {
619 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
620 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
625 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
626 .map(function(g) { return g.toDataURL(); })
627 .filter(function(g) { return g != 'about:blank'; });
630 html = this.cleanWordChars(html);
632 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
635 var sn = this.getParentElement();
636 // check if d contains a table, and prevent nesting??
637 //Roo.log(d.getElementsByTagName('table'));
639 //Roo.log(sn.closest('table'));
640 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
642 this.insertAtCursor("You can not nest tables");
643 //Roo.log("prevent?"); // fixme -
649 if (images.length > 0) {
650 // replace all v:imagedata - with img.
651 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
652 Roo.each(ar, function(node) {
653 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
654 node.parentNode.removeChild(node);
658 Roo.each(d.getElementsByTagName('img'), function(img, i) {
659 img.setAttribute('src', images[i]);
662 if (this.autoClean) {
663 new Roo.htmleditor.FilterWord({ node : d });
665 new Roo.htmleditor.FilterStyleToTag({ node : d });
666 new Roo.htmleditor.FilterAttributes({
668 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
669 attrib_clean : ['href', 'src' ]
671 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
673 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
674 new Roo.htmleditor.FilterParagraph({ node : d });
675 new Roo.htmleditor.FilterSpan({ node : d });
676 new Roo.htmleditor.FilterLongBr({ node : d });
677 new Roo.htmleditor.FilterComment({ node : d });
681 if (this.enableBlocks) {
683 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
684 if (img.closest('figure')) { // assume!! that it's aready
687 var fig = new Roo.htmleditor.BlockFigure({
690 fig.updateElement(img); // replace it..
696 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
697 if (this.enableBlocks) {
698 Roo.htmleditor.Block.initAll(this.doc.body);
703 this.owner.fireEvent('paste', this);
705 // default behaveiour should be our local cleanup paste? (optional?)
706 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
707 //this.owner.fireEvent('paste', e, v);
710 onDestroy : function(){
716 //for (var i =0; i < this.toolbars.length;i++) {
717 // // fixme - ask toolbars for heights?
718 // this.toolbars[i].onDestroy();
721 //this.wrap.dom.innerHTML = '';
722 //this.wrap.remove();
727 onFirstFocus : function(){
730 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
732 this.activated = true;
735 if(Roo.isGecko){ // prevent silly gecko errors
737 var s = this.win.getSelection();
738 if(!s.focusNode || s.focusNode.nodeType != 3){
739 var r = s.getRangeAt(0);
740 r.selectNodeContents((this.doc.body || this.doc.documentElement));
745 this.execCmd('useCSS', true);
746 this.execCmd('styleWithCSS', false);
749 this.owner.fireEvent('activate', this);
753 adjustFont: function(btn){
754 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
755 //if(Roo.isSafari){ // safari
758 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
759 if(Roo.isSafari){ // safari
760 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
761 v = (v < 10) ? 10 : v;
762 v = (v > 48) ? 48 : v;
763 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
768 v = Math.max(1, v+adjust);
770 this.execCmd('FontSize', v );
773 onEditorEvent : function(e)
777 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
778 return; // we do not handle this.. (undo manager does..)
780 // in theory this detects if the last element is not a br, then we try and do that.
781 // its so clicking in space at bottom triggers adding a br and moving the cursor.
783 e.target.nodeName == 'BODY' &&
784 e.type == "mouseup" &&
785 this.doc.body.lastChild
787 var lc = this.doc.body.lastChild;
788 // gtx-trans is google translate plugin adding crap.
789 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
790 lc = lc.previousSibling;
792 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
793 // if last element is <BR> - then dont do anything.
795 var ns = this.doc.createElement('br');
796 this.doc.body.appendChild(ns);
797 range = this.doc.createRange();
798 range.setStartAfter(ns);
799 range.collapse(true);
800 var sel = this.win.getSelection();
801 sel.removeAllRanges();
808 this.fireEditorEvent(e);
809 // this.updateToolbar();
810 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
813 fireEditorEvent: function(e)
815 this.owner.fireEvent('editorevent', this, e);
818 insertTag : function(tg)
820 // could be a bit smarter... -> wrap the current selected tRoo..
821 if (tg.toLowerCase() == 'span' ||
822 tg.toLowerCase() == 'code' ||
823 tg.toLowerCase() == 'sup' ||
824 tg.toLowerCase() == 'sub'
827 range = this.createRange(this.getSelection());
828 var wrappingNode = this.doc.createElement(tg.toLowerCase());
829 wrappingNode.appendChild(range.extractContents());
830 range.insertNode(wrappingNode);
837 this.execCmd("formatblock", tg);
838 this.undoManager.addEvent();
841 insertText : function(txt)
845 var range = this.createRange();
846 range.deleteContents();
847 //alert(Sender.getAttribute('label'));
849 range.insertNode(this.doc.createTextNode(txt));
850 this.undoManager.addEvent();
856 * Executes a Midas editor command on the editor document and performs necessary focus and
857 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
858 * @param {String} cmd The Midas command
859 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
861 relayCmd : function(cmd, value)
867 case 'justifycenter':
868 // if we are in a cell, then we will adjust the
869 var n = this.getParentElement();
870 var td = n.closest('td');
872 var bl = Roo.htmleditor.Block.factory(td);
873 bl.textAlign = cmd.replace('justify','');
875 this.owner.fireEvent('editorevent', this);
878 this.execCmd('styleWithCSS', true); //
882 // if there is no selection, then we insert, and set the curson inside it..
883 this.execCmd('styleWithCSS', false);
893 this.execCmd(cmd, value);
894 this.owner.fireEvent('editorevent', this);
895 //this.updateToolbar();
896 this.owner.deferFocus();
900 * Executes a Midas editor command directly on the editor document.
901 * For visual commands, you should use {@link #relayCmd} instead.
902 * <b>This should only be called after the editor is initialized.</b>
903 * @param {String} cmd The Midas command
904 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
906 execCmd : function(cmd, value){
907 this.doc.execCommand(cmd, false, value === undefined ? null : value);
914 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
916 * @param {String} text | dom node..
918 insertAtCursor : function(text)
925 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
929 // from jquery ui (MIT licenced)
933 if (win.getSelection && win.getSelection().getRangeAt) {
935 // delete the existing?
937 this.createRange(this.getSelection()).deleteContents();
938 range = win.getSelection().getRangeAt(0);
939 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
940 range.insertNode(node);
941 range = range.cloneRange();
942 range.collapse(false);
944 win.getSelection().removeAllRanges();
945 win.getSelection().addRange(range);
949 } else if (win.document.selection && win.document.selection.createRange) {
950 // no firefox support
951 var txt = typeof(text) == 'string' ? text : text.outerHTML;
952 win.document.selection.createRange().pasteHTML(txt);
955 // no firefox support
956 var txt = typeof(text) == 'string' ? text : text.outerHTML;
957 this.execCmd('InsertHTML', txt);
965 mozKeyPress : function(e){
967 var c = e.getCharCode(), cmd;
970 c = String.fromCharCode(c).toLowerCase();
984 // this.cleanUpPaste.defer(100, this);
1002 fixKeys : function(){ // load time branching for fastest keydown performance
1007 var k = e.getKey(), r;
1010 r = this.doc.selection.createRange();
1013 r.pasteHTML('    ');
1018 /// this is handled by Roo.htmleditor.KeyEnter
1021 r = this.doc.selection.createRange();
1023 var target = r.parentElement();
1024 if(!target || target.tagName.toLowerCase() != 'li'){
1026 r.pasteHTML('<br/>');
1033 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1034 // this.cleanUpPaste.defer(100, this);
1040 }else if(Roo.isOpera){
1046 this.execCmd('InsertHTML','    ');
1050 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1051 // this.cleanUpPaste.defer(100, this);
1056 }else if(Roo.isSafari){
1062 this.execCmd('InsertText','\t');
1066 this.mozKeyPress(e);
1068 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1069 // this.cleanUpPaste.defer(100, this);
1077 getAllAncestors: function()
1079 var p = this.getSelectedNode();
1082 a.push(p); // push blank onto stack..
1083 p = this.getParentElement();
1087 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1091 a.push(this.doc.body);
1095 lastSelNode : false,
1098 getSelection : function()
1100 this.assignDocWin();
1101 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1105 * @param {DomElement} node the node to select
1107 selectNode : function(node, collapse)
1109 var nodeRange = node.ownerDocument.createRange();
1111 nodeRange.selectNode(node);
1113 nodeRange.selectNodeContents(node);
1115 if (collapse === true) {
1116 nodeRange.collapse(true);
1119 var s = this.win.getSelection();
1120 s.removeAllRanges();
1121 s.addRange(nodeRange);
1124 getSelectedNode: function()
1126 // this may only work on Gecko!!!
1128 // should we cache this!!!!
1132 var range = this.createRange(this.getSelection()).cloneRange();
1135 var parent = range.parentElement();
1137 var testRange = range.duplicate();
1138 testRange.moveToElementText(parent);
1139 if (testRange.inRange(range)) {
1142 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1145 parent = parent.parentElement;
1150 // is ancestor a text element.
1151 var ac = range.commonAncestorContainer;
1152 if (ac.nodeType == 3) {
1156 var ar = ac.childNodes;
1159 var other_nodes = [];
1160 var has_other_nodes = false;
1161 for (var i=0;i<ar.length;i++) {
1162 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1165 // fullly contained node.
1167 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1172 // probably selected..
1173 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1174 other_nodes.push(ar[i]);
1178 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1183 has_other_nodes = true;
1185 if (!nodes.length && other_nodes.length) {
1188 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1196 createRange: function(sel)
1198 // this has strange effects when using with
1199 // top toolbar - not sure if it's a great idea.
1200 //this.editor.contentWindow.focus();
1201 if (typeof sel != "undefined") {
1203 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1205 return this.doc.createRange();
1208 return this.doc.createRange();
1211 getParentElement: function()
1214 this.assignDocWin();
1215 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1217 var range = this.createRange(sel);
1220 var p = range.commonAncestorContainer;
1221 while (p.nodeType == 3) { // text node
1232 * Range intersection.. the hard stuff...
1236 * [ -- selected range --- ]
1240 * if end is before start or hits it. fail.
1241 * if start is after end or hits it fail.
1243 * if either hits (but other is outside. - then it's not
1249 // @see http://www.thismuchiknow.co.uk/?p=64.
1250 rangeIntersectsNode : function(range, node)
1252 var nodeRange = node.ownerDocument.createRange();
1254 nodeRange.selectNode(node);
1256 nodeRange.selectNodeContents(node);
1259 var rangeStartRange = range.cloneRange();
1260 rangeStartRange.collapse(true);
1262 var rangeEndRange = range.cloneRange();
1263 rangeEndRange.collapse(false);
1265 var nodeStartRange = nodeRange.cloneRange();
1266 nodeStartRange.collapse(true);
1268 var nodeEndRange = nodeRange.cloneRange();
1269 nodeEndRange.collapse(false);
1271 return rangeStartRange.compareBoundaryPoints(
1272 Range.START_TO_START, nodeEndRange) == -1 &&
1273 rangeEndRange.compareBoundaryPoints(
1274 Range.START_TO_START, nodeStartRange) == 1;
1278 rangeCompareNode : function(range, node)
1280 var nodeRange = node.ownerDocument.createRange();
1282 nodeRange.selectNode(node);
1284 nodeRange.selectNodeContents(node);
1288 range.collapse(true);
1290 nodeRange.collapse(true);
1292 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1293 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1295 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1297 var nodeIsBefore = ss == 1;
1298 var nodeIsAfter = ee == -1;
1300 if (nodeIsBefore && nodeIsAfter) {
1303 if (!nodeIsBefore && nodeIsAfter) {
1304 return 1; //right trailed.
1307 if (nodeIsBefore && !nodeIsAfter) {
1308 return 2; // left trailed.
1314 cleanWordChars : function(input) {// change the chars to hex code
1317 [ 8211, "–" ],
1318 [ 8212, "—" ],
1327 Roo.each(swapCodes, function(sw) {
1328 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1330 output = output.replace(swapper, sw[1]);
1340 cleanUpChild : function (node)
1343 new Roo.htmleditor.FilterComment({node : node});
1344 new Roo.htmleditor.FilterAttributes({
1346 attrib_black : this.ablack,
1347 attrib_clean : this.aclean,
1348 style_white : this.cwhite,
1349 style_black : this.cblack
1351 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1352 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1358 * Clean up MS wordisms...
1359 * @deprecated - use filter directly
1361 cleanWord : function(node)
1363 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1364 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1371 * @deprecated - use filters
1373 cleanTableWidths : function(node)
1375 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1382 applyBlacklists : function()
1384 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1385 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1387 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1388 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1389 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1393 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1394 if (b.indexOf(tag) > -1) {
1397 this.white.push(tag);
1401 Roo.each(w, function(tag) {
1402 if (b.indexOf(tag) > -1) {
1405 if (this.white.indexOf(tag) > -1) {
1408 this.white.push(tag);
1413 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1414 if (w.indexOf(tag) > -1) {
1417 this.black.push(tag);
1421 Roo.each(b, function(tag) {
1422 if (w.indexOf(tag) > -1) {
1425 if (this.black.indexOf(tag) > -1) {
1428 this.black.push(tag);
1433 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1434 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1438 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1439 if (b.indexOf(tag) > -1) {
1442 this.cwhite.push(tag);
1446 Roo.each(w, function(tag) {
1447 if (b.indexOf(tag) > -1) {
1450 if (this.cwhite.indexOf(tag) > -1) {
1453 this.cwhite.push(tag);
1458 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1459 if (w.indexOf(tag) > -1) {
1462 this.cblack.push(tag);
1466 Roo.each(b, function(tag) {
1467 if (w.indexOf(tag) > -1) {
1470 if (this.cblack.indexOf(tag) > -1) {
1473 this.cblack.push(tag);
1478 setStylesheets : function(stylesheets)
1480 if(typeof(stylesheets) == 'string'){
1481 Roo.get(this.iframe.contentDocument.head).createChild({
1492 Roo.each(stylesheets, function(s) {
1497 Roo.get(_this.iframe.contentDocument.head).createChild({
1509 updateLanguage : function()
1511 if (!this.iframe || !this.iframe.contentDocument) {
1514 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1518 removeStylesheets : function()
1522 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1527 setStyle : function(style)
1529 Roo.get(this.iframe.contentDocument.head).createChild({
1538 // hide stuff that is not compatible
1556 * @cfg {String} fieldClass @hide
1559 * @cfg {String} focusClass @hide
1562 * @cfg {String} autoCreate @hide
1565 * @cfg {String} inputType @hide
1568 * @cfg {String} invalidClass @hide
1571 * @cfg {String} invalidText @hide
1574 * @cfg {String} msgFx @hide
1577 * @cfg {String} validateOnBlur @hide
1581 Roo.HtmlEditorCore.white = [
1582 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1584 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1585 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1586 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1587 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1588 'TABLE', 'UL', 'XMP',
1590 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1593 'DIR', 'MENU', 'OL', 'UL', 'DL',
1599 Roo.HtmlEditorCore.black = [
1600 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1602 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1603 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1604 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1605 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1606 //'FONT' // CLEAN LATER..
1607 'COLGROUP', 'COL' // messy tables.
1611 Roo.HtmlEditorCore.clean = [ // ?? needed???
1612 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1614 Roo.HtmlEditorCore.tag_remove = [
1619 Roo.HtmlEditorCore.ablack = [
1623 Roo.HtmlEditorCore.aclean = [
1624 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1628 Roo.HtmlEditorCore.pwhite= [
1629 'http', 'https', 'mailto'
1632 // white listed style attributes.
1633 Roo.HtmlEditorCore.cwhite= [
1634 // 'text-align', /// default is to allow most things..
1640 // black listed style attributes.
1641 Roo.HtmlEditorCore.cblack= [
1642 // 'font-size' -- this can be set by the project