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);
442 var lc = this.doc.body.lastChild;
443 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
444 // add an extra line at the end.
445 this.doc.body.appendChild(this.doc.createElement('br'));
453 deferFocus : function(){
454 this.focus.defer(10, this);
459 if(this.win && !this.sourceEditMode){
466 assignDocWin: function()
468 var iframe = this.iframe;
471 this.doc = iframe.contentWindow.document;
472 this.win = iframe.contentWindow;
474 // if (!Roo.get(this.frameId)) {
477 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
478 // this.win = Roo.get(this.frameId).dom.contentWindow;
480 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
484 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
485 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
490 initEditor : function(){
491 //console.log("INIT EDITOR");
496 this.doc.designMode="on";
498 this.doc.write(this.getDocMarkup());
501 var dbody = (this.doc.body || this.doc.documentElement);
502 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
503 // this copies styles from the containing element into thsi one..
504 // not sure why we need all of this..
505 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
507 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
508 //ss['background-attachment'] = 'fixed'; // w3c
509 dbody.bgProperties = 'fixed'; // ie
510 //Roo.DomHelper.applyStyles(dbody, ss);
511 Roo.EventManager.on(this.doc, {
512 //'mousedown': this.onEditorEvent,
513 'mouseup': this.onEditorEvent,
514 'dblclick': this.onEditorEvent,
515 'click': this.onEditorEvent,
516 'keyup': this.onEditorEvent,
521 Roo.EventManager.on(this.doc, {
522 'paste': this.onPasteEvent,
526 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
529 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
530 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
532 this.initialized = true;
535 // initialize special key events - enter
536 new Roo.htmleditor.KeyEnter({core : this});
540 this.owner.fireEvent('initialize', this);
544 onPasteEvent : function(e,v)
546 // I think we better assume paste is going to be a dirty load of rubish from word..
548 // even pasting into a 'email version' of this widget will have to clean up that mess.
549 var cd = (e.browserEvent.clipboardData || window.clipboardData);
551 // check what type of paste - if it's an image, then handle it differently.
552 if (cd.files.length > 0) {
554 var urlAPI = (window.createObjectURL && window) ||
555 (window.URL && URL.revokeObjectURL && URL) ||
556 (window.webkitURL && webkitURL);
558 var url = urlAPI.createObjectURL( cd.files[0]);
559 this.insertAtCursor('<img src=" + url + ">');
563 var html = cd.getData('text/html'); // clipboard event
564 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
565 var images = parser.doc ? parser.doc.getElementsByType('pict') : [];
569 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable)/); }) // ignore headers
570 .map(function(g) { return g.toDataURL(); });
573 html = this.cleanWordChars(html);
575 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
578 var sn = this.getParentElement();
579 // check if d contains a table, and prevent nesting??
580 //Roo.log(d.getElementsByTagName('table'));
582 //Roo.log(sn.closest('table'));
583 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
585 this.insertAtCursor("You can not nest tables");
586 //Roo.log("prevent?"); // fixme -
590 if (images.length > 0) {
591 Roo.each(d.getElementsByTagName('img'), function(img, i) {
592 img.setAttribute('src', images[i]);
595 if (this.autoClean) {
596 new Roo.htmleditor.FilterStyleToTag({ node : d });
597 new Roo.htmleditor.FilterAttributes({
599 attrib_white : ['href', 'src', 'name', 'align'],
600 attrib_clean : ['href', 'src' ]
602 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
604 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT' ]} );
605 new Roo.htmleditor.FilterParagraph({ node : d });
606 new Roo.htmleditor.FilterSpan({ node : d });
607 new Roo.htmleditor.FilterLongBr({ node : d });
609 if (this.enableBlocks) {
611 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
612 if (img.closest('figure')) { // assume!! that it's aready
615 var fig = new Roo.htmleditor.BlockFigure({
618 fig.updateElement(img); // replace it..
624 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
625 if (this.enableBlocks) {
626 Roo.htmleditor.Block.initAll(this.doc.body);
632 // default behaveiour should be our local cleanup paste? (optional?)
633 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
634 //this.owner.fireEvent('paste', e, v);
637 onDestroy : function(){
643 //for (var i =0; i < this.toolbars.length;i++) {
644 // // fixme - ask toolbars for heights?
645 // this.toolbars[i].onDestroy();
648 //this.wrap.dom.innerHTML = '';
649 //this.wrap.remove();
654 onFirstFocus : function(){
657 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
659 this.activated = true;
662 if(Roo.isGecko){ // prevent silly gecko errors
664 var s = this.win.getSelection();
665 if(!s.focusNode || s.focusNode.nodeType != 3){
666 var r = s.getRangeAt(0);
667 r.selectNodeContents((this.doc.body || this.doc.documentElement));
672 this.execCmd('useCSS', true);
673 this.execCmd('styleWithCSS', false);
676 this.owner.fireEvent('activate', this);
680 adjustFont: function(btn){
681 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
682 //if(Roo.isSafari){ // safari
685 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
686 if(Roo.isSafari){ // safari
687 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
688 v = (v < 10) ? 10 : v;
689 v = (v > 48) ? 48 : v;
690 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
695 v = Math.max(1, v+adjust);
697 this.execCmd('FontSize', v );
700 onEditorEvent : function(e)
703 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
704 return; // we do not handle this.. (undo manager does..)
706 // in theory this detects if the last element is not a br, then we try and do that.
707 // its so clicking in space at bottom triggers adding a br and moving the cursor.
709 e.target.nodeName == 'BODY' &&
710 e.type == "mouseup" &&
711 this.doc.body.lastChild
713 var lc = this.doc.body.lastChild;
714 // gtx-trans is google translate plugin adding crap.
715 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
716 lc = lc.previousSibling;
718 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
719 // if last element is <BR> - then dont do anything.
721 var ns = this.doc.createElement('br');
722 this.doc.body.appendChild(ns);
723 range = this.doc.createRange();
724 range.setStartAfter(ns);
725 range.collapse(true);
726 var sel = this.win.getSelection();
727 sel.removeAllRanges();
734 this.fireEditorEvent(e);
735 // this.updateToolbar();
736 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
739 fireEditorEvent: function(e)
741 this.owner.fireEvent('editorevent', this, e);
744 insertTag : function(tg)
746 // could be a bit smarter... -> wrap the current selected tRoo..
747 if (tg.toLowerCase() == 'span' ||
748 tg.toLowerCase() == 'code' ||
749 tg.toLowerCase() == 'sup' ||
750 tg.toLowerCase() == 'sub'
753 range = this.createRange(this.getSelection());
754 var wrappingNode = this.doc.createElement(tg.toLowerCase());
755 wrappingNode.appendChild(range.extractContents());
756 range.insertNode(wrappingNode);
763 this.execCmd("formatblock", tg);
764 this.undoManager.addEvent();
767 insertText : function(txt)
771 var range = this.createRange();
772 range.deleteContents();
773 //alert(Sender.getAttribute('label'));
775 range.insertNode(this.doc.createTextNode(txt));
776 this.undoManager.addEvent();
782 * Executes a Midas editor command on the editor document and performs necessary focus and
783 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
784 * @param {String} cmd The Midas command
785 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
787 relayCmd : function(cmd, value)
793 case 'justifycenter':
794 // if we are in a cell, then we will adjust the
795 var n = this.getParentElement();
796 var td = n.closest('td');
798 var bl = Roo.htmleditor.Block.factory(td);
799 bl.textAlign = cmd.replace('justify','');
801 this.owner.fireEvent('editorevent', this);
804 this.execCmd('styleWithCSS', true); //
808 // if there is no selection, then we insert, and set the curson inside it..
809 this.execCmd('styleWithCSS', false);
819 this.execCmd(cmd, value);
820 this.owner.fireEvent('editorevent', this);
821 //this.updateToolbar();
822 this.owner.deferFocus();
826 * Executes a Midas editor command directly on the editor document.
827 * For visual commands, you should use {@link #relayCmd} instead.
828 * <b>This should only be called after the editor is initialized.</b>
829 * @param {String} cmd The Midas command
830 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
832 execCmd : function(cmd, value){
833 this.doc.execCommand(cmd, false, value === undefined ? null : value);
840 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
842 * @param {String} text | dom node..
844 insertAtCursor : function(text)
851 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
855 // from jquery ui (MIT licenced)
859 if (win.getSelection && win.getSelection().getRangeAt) {
861 // delete the existing?
863 this.createRange(this.getSelection()).deleteContents();
864 range = win.getSelection().getRangeAt(0);
865 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
866 range.insertNode(node);
867 range = range.cloneRange();
868 range.collapse(false);
870 win.getSelection().removeAllRanges();
871 win.getSelection().addRange(range);
875 } else if (win.document.selection && win.document.selection.createRange) {
876 // no firefox support
877 var txt = typeof(text) == 'string' ? text : text.outerHTML;
878 win.document.selection.createRange().pasteHTML(txt);
881 // no firefox support
882 var txt = typeof(text) == 'string' ? text : text.outerHTML;
883 this.execCmd('InsertHTML', txt);
891 mozKeyPress : function(e){
893 var c = e.getCharCode(), cmd;
896 c = String.fromCharCode(c).toLowerCase();
910 // this.cleanUpPaste.defer(100, this);
928 fixKeys : function(){ // load time branching for fastest keydown performance
933 var k = e.getKey(), r;
936 r = this.doc.selection.createRange();
939 r.pasteHTML('    ');
944 /// this is handled by Roo.htmleditor.KeyEnter
947 r = this.doc.selection.createRange();
949 var target = r.parentElement();
950 if(!target || target.tagName.toLowerCase() != 'li'){
952 r.pasteHTML('<br/>');
959 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
960 // this.cleanUpPaste.defer(100, this);
966 }else if(Roo.isOpera){
972 this.execCmd('InsertHTML','    ');
976 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
977 // this.cleanUpPaste.defer(100, this);
982 }else if(Roo.isSafari){
988 this.execCmd('InsertText','\t');
994 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
995 // this.cleanUpPaste.defer(100, this);
1003 getAllAncestors: function()
1005 var p = this.getSelectedNode();
1008 a.push(p); // push blank onto stack..
1009 p = this.getParentElement();
1013 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1017 a.push(this.doc.body);
1021 lastSelNode : false,
1024 getSelection : function()
1026 this.assignDocWin();
1027 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1031 * @param {DomElement} node the node to select
1033 selectNode : function(node, collapse)
1035 var nodeRange = node.ownerDocument.createRange();
1037 nodeRange.selectNode(node);
1039 nodeRange.selectNodeContents(node);
1041 if (collapse === true) {
1042 nodeRange.collapse(true);
1045 var s = this.win.getSelection();
1046 s.removeAllRanges();
1047 s.addRange(nodeRange);
1050 getSelectedNode: function()
1052 // this may only work on Gecko!!!
1054 // should we cache this!!!!
1058 var range = this.createRange(this.getSelection()).cloneRange();
1061 var parent = range.parentElement();
1063 var testRange = range.duplicate();
1064 testRange.moveToElementText(parent);
1065 if (testRange.inRange(range)) {
1068 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1071 parent = parent.parentElement;
1076 // is ancestor a text element.
1077 var ac = range.commonAncestorContainer;
1078 if (ac.nodeType == 3) {
1082 var ar = ac.childNodes;
1085 var other_nodes = [];
1086 var has_other_nodes = false;
1087 for (var i=0;i<ar.length;i++) {
1088 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1091 // fullly contained node.
1093 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1098 // probably selected..
1099 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1100 other_nodes.push(ar[i]);
1104 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1109 has_other_nodes = true;
1111 if (!nodes.length && other_nodes.length) {
1114 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1122 createRange: function(sel)
1124 // this has strange effects when using with
1125 // top toolbar - not sure if it's a great idea.
1126 //this.editor.contentWindow.focus();
1127 if (typeof sel != "undefined") {
1129 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1131 return this.doc.createRange();
1134 return this.doc.createRange();
1137 getParentElement: function()
1140 this.assignDocWin();
1141 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1143 var range = this.createRange(sel);
1146 var p = range.commonAncestorContainer;
1147 while (p.nodeType == 3) { // text node
1158 * Range intersection.. the hard stuff...
1162 * [ -- selected range --- ]
1166 * if end is before start or hits it. fail.
1167 * if start is after end or hits it fail.
1169 * if either hits (but other is outside. - then it's not
1175 // @see http://www.thismuchiknow.co.uk/?p=64.
1176 rangeIntersectsNode : function(range, node)
1178 var nodeRange = node.ownerDocument.createRange();
1180 nodeRange.selectNode(node);
1182 nodeRange.selectNodeContents(node);
1185 var rangeStartRange = range.cloneRange();
1186 rangeStartRange.collapse(true);
1188 var rangeEndRange = range.cloneRange();
1189 rangeEndRange.collapse(false);
1191 var nodeStartRange = nodeRange.cloneRange();
1192 nodeStartRange.collapse(true);
1194 var nodeEndRange = nodeRange.cloneRange();
1195 nodeEndRange.collapse(false);
1197 return rangeStartRange.compareBoundaryPoints(
1198 Range.START_TO_START, nodeEndRange) == -1 &&
1199 rangeEndRange.compareBoundaryPoints(
1200 Range.START_TO_START, nodeStartRange) == 1;
1204 rangeCompareNode : function(range, node)
1206 var nodeRange = node.ownerDocument.createRange();
1208 nodeRange.selectNode(node);
1210 nodeRange.selectNodeContents(node);
1214 range.collapse(true);
1216 nodeRange.collapse(true);
1218 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1219 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1221 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1223 var nodeIsBefore = ss == 1;
1224 var nodeIsAfter = ee == -1;
1226 if (nodeIsBefore && nodeIsAfter) {
1229 if (!nodeIsBefore && nodeIsAfter) {
1230 return 1; //right trailed.
1233 if (nodeIsBefore && !nodeIsAfter) {
1234 return 2; // left trailed.
1240 cleanWordChars : function(input) {// change the chars to hex code
1243 [ 8211, "–" ],
1244 [ 8212, "—" ],
1253 Roo.each(swapCodes, function(sw) {
1254 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1256 output = output.replace(swapper, sw[1]);
1266 cleanUpChild : function (node)
1269 new Roo.htmleditor.FilterComment({node : node});
1270 new Roo.htmleditor.FilterAttributes({
1272 attrib_black : this.ablack,
1273 attrib_clean : this.aclean,
1274 style_white : this.cwhite,
1275 style_black : this.cblack
1277 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1278 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1284 * Clean up MS wordisms...
1285 * @deprecated - use filter directly
1287 cleanWord : function(node)
1289 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1296 * @deprecated - use filters
1298 cleanTableWidths : function(node)
1300 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1307 applyBlacklists : function()
1309 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1310 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1312 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1313 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1314 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1318 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1319 if (b.indexOf(tag) > -1) {
1322 this.white.push(tag);
1326 Roo.each(w, function(tag) {
1327 if (b.indexOf(tag) > -1) {
1330 if (this.white.indexOf(tag) > -1) {
1333 this.white.push(tag);
1338 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1339 if (w.indexOf(tag) > -1) {
1342 this.black.push(tag);
1346 Roo.each(b, function(tag) {
1347 if (w.indexOf(tag) > -1) {
1350 if (this.black.indexOf(tag) > -1) {
1353 this.black.push(tag);
1358 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1359 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1363 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1364 if (b.indexOf(tag) > -1) {
1367 this.cwhite.push(tag);
1371 Roo.each(w, function(tag) {
1372 if (b.indexOf(tag) > -1) {
1375 if (this.cwhite.indexOf(tag) > -1) {
1378 this.cwhite.push(tag);
1383 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1384 if (w.indexOf(tag) > -1) {
1387 this.cblack.push(tag);
1391 Roo.each(b, function(tag) {
1392 if (w.indexOf(tag) > -1) {
1395 if (this.cblack.indexOf(tag) > -1) {
1398 this.cblack.push(tag);
1403 setStylesheets : function(stylesheets)
1405 if(typeof(stylesheets) == 'string'){
1406 Roo.get(this.iframe.contentDocument.head).createChild({
1417 Roo.each(stylesheets, function(s) {
1422 Roo.get(_this.iframe.contentDocument.head).createChild({
1434 updateLanguage : function()
1436 Roo.get(_this.ifream.content.body).attr("lang", this.language);
1440 removeStylesheets : function()
1444 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1449 setStyle : function(style)
1451 Roo.get(this.iframe.contentDocument.head).createChild({
1460 // hide stuff that is not compatible
1478 * @cfg {String} fieldClass @hide
1481 * @cfg {String} focusClass @hide
1484 * @cfg {String} autoCreate @hide
1487 * @cfg {String} inputType @hide
1490 * @cfg {String} invalidClass @hide
1493 * @cfg {String} invalidText @hide
1496 * @cfg {String} msgFx @hide
1499 * @cfg {String} validateOnBlur @hide
1503 Roo.HtmlEditorCore.white = [
1504 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1506 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1507 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1508 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1509 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1510 'TABLE', 'UL', 'XMP',
1512 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1515 'DIR', 'MENU', 'OL', 'UL', 'DL',
1521 Roo.HtmlEditorCore.black = [
1522 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1524 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1525 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1526 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1527 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1528 //'FONT' // CLEAN LATER..
1529 'COLGROUP', 'COL' // messy tables.
1532 Roo.HtmlEditorCore.clean = [ // ?? needed???
1533 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1535 Roo.HtmlEditorCore.tag_remove = [
1540 Roo.HtmlEditorCore.ablack = [
1544 Roo.HtmlEditorCore.aclean = [
1545 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1549 Roo.HtmlEditorCore.pwhite= [
1550 'http', 'https', 'mailto'
1553 // white listed style attributes.
1554 Roo.HtmlEditorCore.cwhite= [
1555 // 'text-align', /// default is to allow most things..
1561 // black listed style attributes.
1562 Roo.HtmlEditorCore.cblack= [
1563 // 'font-size' -- this can be set by the project