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 var cls = 'roo-htmleditor-body';
201 if(this.bodyCls.length){
202 cls += ' ' + this.bodyCls;
205 return '<html><head>' + st +
206 //<style type="text/css">' +
207 //'body{border:0;margin:0;padding:3px;height:98%;cursor:text;}' +
209 ' </head><body contenteditable="true" data-enable-grammerly="true" class="' + cls + '"></body></html>';
213 onRender : function(ct, position)
216 //Roo.HtmlEditorCore.superclass.onRender.call(this, ct, position);
217 this.el = this.owner.inputEl ? this.owner.inputEl() : this.owner.el;
220 this.el.dom.style.border = '0 none';
221 this.el.dom.setAttribute('tabIndex', -1);
222 this.el.addClass('x-hidden hide');
226 if(Roo.isIE){ // fix IE 1px bogus margin
227 this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;')
231 this.frameId = Roo.id();
235 var iframe = this.owner.wrap.createChild({
237 cls: 'form-control', // bootstrap..
241 'src' : Roo.SSL_SECURE_URL ? Roo.SSL_SECURE_URL : "javascript:false"
246 this.iframe = iframe.dom;
250 this.doc.designMode = 'on';
253 this.doc.write(this.getDocMarkup());
257 var task = { // must defer to wait for browser to be ready
259 //console.log("run task?" + this.doc.readyState);
261 if(this.doc.body || this.doc.readyState == 'complete'){
263 this.doc.designMode="on";
268 Roo.TaskMgr.stop(task);
269 this.initEditor.defer(10, this);
276 Roo.TaskMgr.start(task);
281 onResize : function(w, h)
283 Roo.log('resize: ' +w + ',' + h );
284 //Roo.HtmlEditorCore.superclass.onResize.apply(this, arguments);
288 if(typeof w == 'number'){
290 this.iframe.style.width = w + 'px';
292 if(typeof h == 'number'){
294 this.iframe.style.height = h + 'px';
296 (this.doc.body || this.doc.documentElement).style.height = (h - (this.iframePad*2)) + 'px';
303 * Toggles the editor between standard and source edit mode.
304 * @param {Boolean} sourceEdit (optional) True for source edit, false for standard
306 toggleSourceEdit : function(sourceEditMode){
308 this.sourceEditMode = sourceEditMode === true;
310 if(this.sourceEditMode){
312 Roo.get(this.iframe).addClass(['x-hidden','hide', 'd-none']); //FIXME - what's the BS styles for these
315 Roo.get(this.iframe).removeClass(['x-hidden','hide', 'd-none']);
316 //this.iframe.className = '';
319 //this.setSize(this.owner.wrap.getSize());
320 //this.fireEvent('editmodechange', this, this.sourceEditMode);
327 * Protected method that will not generally be called directly. If you need/want
328 * custom HTML cleanup, this is the method you should override.
329 * @param {String} html The HTML to be cleaned
330 * return {String} The cleaned HTML
332 cleanHtml : function(html){
335 if(Roo.isSafari){ // strip safari nonsense
336 html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, '');
339 if(html == ' '){
346 * HTML Editor -> Textarea
347 * Protected method that will not generally be called directly. Syncs the contents
348 * of the editor iframe with the textarea.
350 syncValue : function()
352 //Roo.log("HtmlEditorCore:syncValue (EDITOR->TEXT)");
353 if(this.initialized){
355 this.undoManager.addEvent();
358 var bd = (this.doc.body || this.doc.documentElement);
362 var div = document.createElement('div');
363 div.innerHTML = bd.innerHTML;
366 if (this.enableBlocks) {
367 new Roo.htmleditor.FilterBlock({ node : div });
372 var html = div.innerHTML;
374 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
375 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
377 html = '<div style="'+m[0]+'">' + html + '</div>';
380 html = this.cleanHtml(html);
381 // fix up the special chars.. normaly like back quotes in word...
382 // however we do not want to do this with chinese..
383 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
385 var cc = match.charCodeAt();
387 // Get the character value, handling surrogate pairs
388 if (match.length == 2) {
389 // It's a surrogate pair, calculate the Unicode code point
390 var high = match.charCodeAt(0) - 0xD800;
391 var low = match.charCodeAt(1) - 0xDC00;
392 cc = (high * 0x400) + low + 0x10000;
394 (cc >= 0x4E00 && cc < 0xA000 ) ||
395 (cc >= 0x3400 && cc < 0x4E00 ) ||
396 (cc >= 0xf900 && cc < 0xfb00 )
401 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
402 return "&#" + cc + ";";
409 if(this.owner.fireEvent('beforesync', this, html) !== false){
410 this.el.dom.value = html;
411 this.owner.fireEvent('sync', this, html);
417 * TEXTAREA -> EDITABLE
418 * Protected method that will not generally be called directly. Pushes the value of the textarea
419 * into the iframe editor.
421 pushValue : function()
423 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
424 if(this.initialized){
425 var v = this.el.dom.value.trim();
428 if(this.owner.fireEvent('beforepush', this, v) !== false){
429 var d = (this.doc.body || this.doc.documentElement);
432 this.el.dom.value = d.innerHTML;
433 this.owner.fireEvent('push', this, v);
435 if (this.autoClean) {
436 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
437 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
440 Roo.htmleditor.Block.initAll(this.doc.body);
441 this.updateLanguage();
443 var lc = this.doc.body.lastChild;
444 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
445 // add an extra line at the end.
446 this.doc.body.appendChild(this.doc.createElement('br'));
454 deferFocus : function(){
455 this.focus.defer(10, this);
460 if(this.win && !this.sourceEditMode){
467 assignDocWin: function()
469 var iframe = this.iframe;
472 this.doc = iframe.contentWindow.document;
473 this.win = iframe.contentWindow;
475 // if (!Roo.get(this.frameId)) {
478 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
479 // this.win = Roo.get(this.frameId).dom.contentWindow;
481 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
485 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
486 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
491 initEditor : function(){
492 //console.log("INIT EDITOR");
497 this.doc.designMode="on";
499 this.doc.write(this.getDocMarkup());
502 var dbody = (this.doc.body || this.doc.documentElement);
503 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
504 // this copies styles from the containing element into thsi one..
505 // not sure why we need all of this..
506 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
508 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
509 //ss['background-attachment'] = 'fixed'; // w3c
510 dbody.bgProperties = 'fixed'; // ie
511 //Roo.DomHelper.applyStyles(dbody, ss);
512 Roo.EventManager.on(this.doc, {
513 'mousedown': this.onMouseDown,
514 'mouseup': this.onEditorEvent,
515 'dblclick': this.onEditorEvent,
516 'click': this.onEditorEvent,
517 'keyup': this.onEditorEvent,
522 Roo.EventManager.on(this.doc, {
523 'paste': this.onPasteEvent,
527 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
530 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
531 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
533 this.initialized = true;
536 // initialize special key events - enter
537 new Roo.htmleditor.KeyEnter({core : this});
541 this.owner.fireEvent('initialize', this);
545 onPasteEvent : function(e,v)
547 // I think we better assume paste is going to be a dirty load of rubish from word..
549 // even pasting into a 'email version' of this widget will have to clean up that mess.
550 var cd = (e.browserEvent.clipboardData || window.clipboardData);
552 // check what type of paste - if it's an image, then handle it differently.
553 if (cd.files.length > 0) {
555 var urlAPI = (window.createObjectURL && window) ||
556 (window.URL && URL.revokeObjectURL && URL) ||
557 (window.webkitURL && webkitURL);
559 var url = urlAPI.createObjectURL( cd.files[0]);
560 this.insertAtCursor('<img src=" + url + ">');
564 var html = cd.getData('text/html'); // clipboard event
565 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
566 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
570 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
571 .map(function(g) { return g.toDataURL(); });
574 html = this.cleanWordChars(html);
576 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
579 var sn = this.getParentElement();
580 // check if d contains a table, and prevent nesting??
581 //Roo.log(d.getElementsByTagName('table'));
583 //Roo.log(sn.closest('table'));
584 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
586 this.insertAtCursor("You can not nest tables");
587 //Roo.log("prevent?"); // fixme -
591 if (images.length > 0) {
592 Roo.each(d.getElementsByTagName('img'), function(img, i) {
593 img.setAttribute('src', images[i]);
596 if (this.autoClean) {
597 new Roo.htmleditor.FilterStyleToTag({ node : d });
598 new Roo.htmleditor.FilterAttributes({
600 attrib_white : ['href', 'src', 'name', 'align'],
601 attrib_clean : ['href', 'src' ]
603 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
605 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
606 new Roo.htmleditor.FilterParagraph({ node : d });
607 new Roo.htmleditor.FilterSpan({ node : d });
608 new Roo.htmleditor.FilterLongBr({ node : d });
610 if (this.enableBlocks) {
612 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
613 if (img.closest('figure')) { // assume!! that it's aready
616 var fig = new Roo.htmleditor.BlockFigure({
619 fig.updateElement(img); // replace it..
625 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
626 if (this.enableBlocks) {
627 Roo.htmleditor.Block.initAll(this.doc.body);
633 // default behaveiour should be our local cleanup paste? (optional?)
634 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
635 //this.owner.fireEvent('paste', e, v);
638 onDestroy : function(){
644 //for (var i =0; i < this.toolbars.length;i++) {
645 // // fixme - ask toolbars for heights?
646 // this.toolbars[i].onDestroy();
649 //this.wrap.dom.innerHTML = '';
650 //this.wrap.remove();
655 onFirstFocus : function(){
658 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
660 this.activated = true;
663 if(Roo.isGecko){ // prevent silly gecko errors
665 var s = this.win.getSelection();
666 if(!s.focusNode || s.focusNode.nodeType != 3){
667 var r = s.getRangeAt(0);
668 r.selectNodeContents((this.doc.body || this.doc.documentElement));
673 this.execCmd('useCSS', true);
674 this.execCmd('styleWithCSS', false);
677 this.owner.fireEvent('activate', this);
681 adjustFont: function(btn){
682 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
683 //if(Roo.isSafari){ // safari
686 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
687 if(Roo.isSafari){ // safari
688 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
689 v = (v < 10) ? 10 : v;
690 v = (v > 48) ? 48 : v;
691 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
696 v = Math.max(1, v+adjust);
698 this.execCmd('FontSize', v );
701 onEditorEvent : function(e)
704 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
705 return; // we do not handle this.. (undo manager does..)
707 // in theory this detects if the last element is not a br, then we try and do that.
708 // its so clicking in space at bottom triggers adding a br and moving the cursor.
710 e.target.nodeName == 'BODY' &&
711 e.type == "mouseup" &&
712 this.doc.body.lastChild
714 var lc = this.doc.body.lastChild;
715 // gtx-trans is google translate plugin adding crap.
716 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
717 lc = lc.previousSibling;
719 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
720 // if last element is <BR> - then dont do anything.
722 var ns = this.doc.createElement('br');
723 this.doc.body.appendChild(ns);
724 range = this.doc.createRange();
725 range.setStartAfter(ns);
726 range.collapse(true);
727 var sel = this.win.getSelection();
728 sel.removeAllRanges();
735 this.fireEditorEvent(e);
736 // this.updateToolbar();
737 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
740 fireEditorEvent: function(e)
742 this.owner.fireEvent('editorevent', this, e);
745 insertTag : function(tg)
747 // could be a bit smarter... -> wrap the current selected tRoo..
748 if (tg.toLowerCase() == 'span' ||
749 tg.toLowerCase() == 'code' ||
750 tg.toLowerCase() == 'sup' ||
751 tg.toLowerCase() == 'sub'
754 range = this.createRange(this.getSelection());
755 var wrappingNode = this.doc.createElement(tg.toLowerCase());
756 wrappingNode.appendChild(range.extractContents());
757 range.insertNode(wrappingNode);
764 this.execCmd("formatblock", tg);
765 this.undoManager.addEvent();
768 insertText : function(txt)
772 var range = this.createRange();
773 range.deleteContents();
774 //alert(Sender.getAttribute('label'));
776 range.insertNode(this.doc.createTextNode(txt));
777 this.undoManager.addEvent();
783 * Executes a Midas editor command on the editor document and performs necessary focus and
784 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
785 * @param {String} cmd The Midas command
786 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
788 relayCmd : function(cmd, value)
794 case 'justifycenter':
795 // if we are in a cell, then we will adjust the
796 var n = this.getParentElement();
797 var td = n.closest('td');
799 var bl = Roo.htmleditor.Block.factory(td);
800 bl.textAlign = cmd.replace('justify','');
802 this.owner.fireEvent('editorevent', this);
805 this.execCmd('styleWithCSS', true); //
809 // if there is no selection, then we insert, and set the curson inside it..
810 this.execCmd('styleWithCSS', false);
820 this.execCmd(cmd, value);
821 this.owner.fireEvent('editorevent', this);
822 //this.updateToolbar();
823 this.owner.deferFocus();
827 * Executes a Midas editor command directly on the editor document.
828 * For visual commands, you should use {@link #relayCmd} instead.
829 * <b>This should only be called after the editor is initialized.</b>
830 * @param {String} cmd The Midas command
831 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
833 execCmd : function(cmd, value){
834 this.doc.execCommand(cmd, false, value === undefined ? null : value);
841 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
843 * @param {String} text | dom node..
845 insertAtCursor : function(text)
852 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
856 // from jquery ui (MIT licenced)
860 if (win.getSelection && win.getSelection().getRangeAt) {
862 // delete the existing?
864 this.createRange(this.getSelection()).deleteContents();
865 range = win.getSelection().getRangeAt(0);
866 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
867 range.insertNode(node);
868 range = range.cloneRange();
869 range.collapse(false);
871 win.getSelection().removeAllRanges();
872 win.getSelection().addRange(range);
876 } else if (win.document.selection && win.document.selection.createRange) {
877 // no firefox support
878 var txt = typeof(text) == 'string' ? text : text.outerHTML;
879 win.document.selection.createRange().pasteHTML(txt);
882 // no firefox support
883 var txt = typeof(text) == 'string' ? text : text.outerHTML;
884 this.execCmd('InsertHTML', txt);
892 mozKeyPress : function(e){
894 var c = e.getCharCode(), cmd;
897 c = String.fromCharCode(c).toLowerCase();
911 // this.cleanUpPaste.defer(100, this);
929 fixKeys : function(){ // load time branching for fastest keydown performance
934 var k = e.getKey(), r;
937 r = this.doc.selection.createRange();
940 r.pasteHTML('    ');
945 /// this is handled by Roo.htmleditor.KeyEnter
948 r = this.doc.selection.createRange();
950 var target = r.parentElement();
951 if(!target || target.tagName.toLowerCase() != 'li'){
953 r.pasteHTML('<br/>');
960 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
961 // this.cleanUpPaste.defer(100, this);
967 }else if(Roo.isOpera){
973 this.execCmd('InsertHTML','    ');
977 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
978 // this.cleanUpPaste.defer(100, this);
983 }else if(Roo.isSafari){
989 this.execCmd('InsertText','\t');
995 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
996 // this.cleanUpPaste.defer(100, this);
1004 getAllAncestors: function()
1006 var p = this.getSelectedNode();
1009 a.push(p); // push blank onto stack..
1010 p = this.getParentElement();
1014 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1018 a.push(this.doc.body);
1022 lastSelNode : false,
1025 getSelection : function()
1027 this.assignDocWin();
1028 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1032 * @param {DomElement} node the node to select
1034 selectNode : function(node, collapse)
1036 var nodeRange = node.ownerDocument.createRange();
1038 nodeRange.selectNode(node);
1040 nodeRange.selectNodeContents(node);
1042 if (collapse === true) {
1043 nodeRange.collapse(true);
1046 var s = this.win.getSelection();
1047 s.removeAllRanges();
1048 s.addRange(nodeRange);
1051 getSelectedNode: function()
1053 // this may only work on Gecko!!!
1055 // should we cache this!!!!
1059 var range = this.createRange(this.getSelection()).cloneRange();
1062 var parent = range.parentElement();
1064 var testRange = range.duplicate();
1065 testRange.moveToElementText(parent);
1066 if (testRange.inRange(range)) {
1069 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1072 parent = parent.parentElement;
1077 // is ancestor a text element.
1078 var ac = range.commonAncestorContainer;
1079 if (ac.nodeType == 3) {
1083 var ar = ac.childNodes;
1086 var other_nodes = [];
1087 var has_other_nodes = false;
1088 for (var i=0;i<ar.length;i++) {
1089 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1092 // fullly contained node.
1094 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1099 // probably selected..
1100 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1101 other_nodes.push(ar[i]);
1105 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1110 has_other_nodes = true;
1112 if (!nodes.length && other_nodes.length) {
1115 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1123 createRange: function(sel)
1125 // this has strange effects when using with
1126 // top toolbar - not sure if it's a great idea.
1127 //this.editor.contentWindow.focus();
1128 if (typeof sel != "undefined") {
1130 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1132 return this.doc.createRange();
1135 return this.doc.createRange();
1138 getParentElement: function()
1141 this.assignDocWin();
1142 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1144 var range = this.createRange(sel);
1147 var p = range.commonAncestorContainer;
1148 while (p.nodeType == 3) { // text node
1159 * Range intersection.. the hard stuff...
1163 * [ -- selected range --- ]
1167 * if end is before start or hits it. fail.
1168 * if start is after end or hits it fail.
1170 * if either hits (but other is outside. - then it's not
1176 // @see http://www.thismuchiknow.co.uk/?p=64.
1177 rangeIntersectsNode : function(range, node)
1179 var nodeRange = node.ownerDocument.createRange();
1181 nodeRange.selectNode(node);
1183 nodeRange.selectNodeContents(node);
1186 var rangeStartRange = range.cloneRange();
1187 rangeStartRange.collapse(true);
1189 var rangeEndRange = range.cloneRange();
1190 rangeEndRange.collapse(false);
1192 var nodeStartRange = nodeRange.cloneRange();
1193 nodeStartRange.collapse(true);
1195 var nodeEndRange = nodeRange.cloneRange();
1196 nodeEndRange.collapse(false);
1198 return rangeStartRange.compareBoundaryPoints(
1199 Range.START_TO_START, nodeEndRange) == -1 &&
1200 rangeEndRange.compareBoundaryPoints(
1201 Range.START_TO_START, nodeStartRange) == 1;
1205 rangeCompareNode : function(range, node)
1207 var nodeRange = node.ownerDocument.createRange();
1209 nodeRange.selectNode(node);
1211 nodeRange.selectNodeContents(node);
1215 range.collapse(true);
1217 nodeRange.collapse(true);
1219 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1220 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1222 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1224 var nodeIsBefore = ss == 1;
1225 var nodeIsAfter = ee == -1;
1227 if (nodeIsBefore && nodeIsAfter) {
1230 if (!nodeIsBefore && nodeIsAfter) {
1231 return 1; //right trailed.
1234 if (nodeIsBefore && !nodeIsAfter) {
1235 return 2; // left trailed.
1241 cleanWordChars : function(input) {// change the chars to hex code
1244 [ 8211, "–" ],
1245 [ 8212, "—" ],
1254 Roo.each(swapCodes, function(sw) {
1255 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1257 output = output.replace(swapper, sw[1]);
1267 cleanUpChild : function (node)
1270 new Roo.htmleditor.FilterComment({node : node});
1271 new Roo.htmleditor.FilterAttributes({
1273 attrib_black : this.ablack,
1274 attrib_clean : this.aclean,
1275 style_white : this.cwhite,
1276 style_black : this.cblack
1278 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1279 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1285 * Clean up MS wordisms...
1286 * @deprecated - use filter directly
1288 cleanWord : function(node)
1290 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1297 * @deprecated - use filters
1299 cleanTableWidths : function(node)
1301 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1308 applyBlacklists : function()
1310 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1311 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1313 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1314 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1315 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1319 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1320 if (b.indexOf(tag) > -1) {
1323 this.white.push(tag);
1327 Roo.each(w, function(tag) {
1328 if (b.indexOf(tag) > -1) {
1331 if (this.white.indexOf(tag) > -1) {
1334 this.white.push(tag);
1339 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1340 if (w.indexOf(tag) > -1) {
1343 this.black.push(tag);
1347 Roo.each(b, function(tag) {
1348 if (w.indexOf(tag) > -1) {
1351 if (this.black.indexOf(tag) > -1) {
1354 this.black.push(tag);
1359 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1360 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1364 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1365 if (b.indexOf(tag) > -1) {
1368 this.cwhite.push(tag);
1372 Roo.each(w, function(tag) {
1373 if (b.indexOf(tag) > -1) {
1376 if (this.cwhite.indexOf(tag) > -1) {
1379 this.cwhite.push(tag);
1384 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1385 if (w.indexOf(tag) > -1) {
1388 this.cblack.push(tag);
1392 Roo.each(b, function(tag) {
1393 if (w.indexOf(tag) > -1) {
1396 if (this.cblack.indexOf(tag) > -1) {
1399 this.cblack.push(tag);
1404 setStylesheets : function(stylesheets)
1406 if(typeof(stylesheets) == 'string'){
1407 Roo.get(this.iframe.contentDocument.head).createChild({
1418 Roo.each(stylesheets, function(s) {
1423 Roo.get(_this.iframe.contentDocument.head).createChild({
1435 updateLanguage : function()
1437 if (!this.iframe || !this.iframe.contentDocument) {
1440 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1444 removeStylesheets : function()
1448 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1453 setStyle : function(style)
1455 Roo.get(this.iframe.contentDocument.head).createChild({
1464 // hide stuff that is not compatible
1482 * @cfg {String} fieldClass @hide
1485 * @cfg {String} focusClass @hide
1488 * @cfg {String} autoCreate @hide
1491 * @cfg {String} inputType @hide
1494 * @cfg {String} invalidClass @hide
1497 * @cfg {String} invalidText @hide
1500 * @cfg {String} msgFx @hide
1503 * @cfg {String} validateOnBlur @hide
1507 Roo.HtmlEditorCore.white = [
1508 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1510 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1511 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1512 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1513 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1514 'TABLE', 'UL', 'XMP',
1516 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1519 'DIR', 'MENU', 'OL', 'UL', 'DL',
1525 Roo.HtmlEditorCore.black = [
1526 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1528 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1529 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1530 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1531 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1532 //'FONT' // CLEAN LATER..
1533 'COLGROUP', 'COL' // messy tables.
1536 Roo.HtmlEditorCore.clean = [ // ?? needed???
1537 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1539 Roo.HtmlEditorCore.tag_remove = [
1544 Roo.HtmlEditorCore.ablack = [
1548 Roo.HtmlEditorCore.aclean = [
1549 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1553 Roo.HtmlEditorCore.pwhite= [
1554 'http', 'https', 'mailto'
1557 // white listed style attributes.
1558 Roo.HtmlEditorCore.cwhite= [
1559 // 'text-align', /// default is to allow most things..
1565 // black listed style attributes.
1566 Roo.HtmlEditorCore.cblack= [
1567 // 'font-size' -- this can be set by the project