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 if (this.autoClean) {
382 var tidy = new Roo.htmleditor.TidySerializer({
385 var html = tidy.serialize(div);
391 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
392 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
394 html = '<div style="'+m[0]+'">' + html + '</div>';
397 html = this.cleanHtml(html);
398 // fix up the special chars.. normaly like back quotes in word...
399 // however we do not want to do this with chinese..
400 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
402 var cc = match.charCodeAt();
404 // Get the character value, handling surrogate pairs
405 if (match.length == 2) {
406 // It's a surrogate pair, calculate the Unicode code point
407 var high = match.charCodeAt(0) - 0xD800;
408 var low = match.charCodeAt(1) - 0xDC00;
409 cc = (high * 0x400) + low + 0x10000;
411 (cc >= 0x4E00 && cc < 0xA000 ) ||
412 (cc >= 0x3400 && cc < 0x4E00 ) ||
413 (cc >= 0xf900 && cc < 0xfb00 )
418 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
419 return "&#" + cc + ";";
426 if(this.owner.fireEvent('beforesync', this, html) !== false){
427 this.el.dom.value = html;
428 this.owner.fireEvent('sync', this, html);
434 * TEXTAREA -> EDITABLE
435 * Protected method that will not generally be called directly. Pushes the value of the textarea
436 * into the iframe editor.
438 pushValue : function()
440 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
441 if(this.initialized){
442 var v = this.el.dom.value.trim();
445 if(this.owner.fireEvent('beforepush', this, v) !== false){
446 var d = (this.doc.body || this.doc.documentElement);
449 this.el.dom.value = d.innerHTML;
450 this.owner.fireEvent('push', this, v);
452 if (this.autoClean) {
453 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
454 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
456 if (this.enableBlocks) {
457 Roo.htmleditor.Block.initAll(this.doc.body);
460 this.updateLanguage();
462 var lc = this.doc.body.lastChild;
463 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
464 // add an extra line at the end.
465 this.doc.body.appendChild(this.doc.createElement('br'));
473 deferFocus : function(){
474 this.focus.defer(10, this);
479 if(this.win && !this.sourceEditMode){
486 assignDocWin: function()
488 var iframe = this.iframe;
491 this.doc = iframe.contentWindow.document;
492 this.win = iframe.contentWindow;
494 // if (!Roo.get(this.frameId)) {
497 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
498 // this.win = Roo.get(this.frameId).dom.contentWindow;
500 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
504 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
505 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
510 initEditor : function(){
511 //console.log("INIT EDITOR");
516 this.doc.designMode="on";
518 this.doc.write(this.getDocMarkup());
521 var dbody = (this.doc.body || this.doc.documentElement);
522 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
523 // this copies styles from the containing element into thsi one..
524 // not sure why we need all of this..
525 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
527 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
528 //ss['background-attachment'] = 'fixed'; // w3c
529 dbody.bgProperties = 'fixed'; // ie
530 dbody.setAttribute("translate", "no");
532 //Roo.DomHelper.applyStyles(dbody, ss);
533 Roo.EventManager.on(this.doc, {
535 'mouseup': this.onEditorEvent,
536 'dblclick': this.onEditorEvent,
537 'click': this.onEditorEvent,
538 'keyup': this.onEditorEvent,
543 Roo.EventManager.on(this.doc, {
544 'paste': this.onPasteEvent,
548 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
551 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
552 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
554 this.initialized = true;
557 // initialize special key events - enter
558 new Roo.htmleditor.KeyEnter({core : this});
562 this.owner.fireEvent('initialize', this);
565 // this is to prevent a href clicks resulting in a redirect?
567 onPasteEvent : function(e,v)
569 // I think we better assume paste is going to be a dirty load of rubish from word..
571 // even pasting into a 'email version' of this widget will have to clean up that mess.
572 var cd = (e.browserEvent.clipboardData || window.clipboardData);
574 // check what type of paste - if it's an image, then handle it differently.
575 if (cd.files && cd.files.length > 0) {
577 var urlAPI = (window.createObjectURL && window) ||
578 (window.URL && URL.revokeObjectURL && URL) ||
579 (window.webkitURL && webkitURL);
581 var url = urlAPI.createObjectURL( cd.files[0]);
582 this.insertAtCursor('<img src=" + url + ">');
585 if (cd.types.indexOf('text/html') < 0 ) {
589 var html = cd.getData('text/html'); // clipboard event
590 if (cd.types.indexOf('text/rtf') > -1) {
591 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
592 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
597 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
598 .map(function(g) { return g.toDataURL(); })
599 .filter(function(g) { return g != 'about:blank'; });
602 html = this.cleanWordChars(html);
604 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
607 var sn = this.getParentElement();
608 // check if d contains a table, and prevent nesting??
609 //Roo.log(d.getElementsByTagName('table'));
611 //Roo.log(sn.closest('table'));
612 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
614 this.insertAtCursor("You can not nest tables");
615 //Roo.log("prevent?"); // fixme -
621 if (images.length > 0) {
622 // replace all v:imagedata - with img.
623 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
624 Roo.each(ar, function(node) {
625 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
626 node.parentNode.removeChild(node);
630 Roo.each(d.getElementsByTagName('img'), function(img, i) {
631 img.setAttribute('src', images[i]);
634 if (this.autoClean) {
635 new Roo.htmleditor.FilterWord({ node : d });
637 new Roo.htmleditor.FilterStyleToTag({ node : d });
638 new Roo.htmleditor.FilterAttributes({
640 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
641 attrib_clean : ['href', 'src' ]
643 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
645 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
646 new Roo.htmleditor.FilterParagraph({ node : d });
647 new Roo.htmleditor.FilterSpan({ node : d });
648 new Roo.htmleditor.FilterLongBr({ node : d });
649 new Roo.htmleditor.FilterComment({ node : d });
653 if (this.enableBlocks) {
655 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
656 if (img.closest('figure')) { // assume!! that it's aready
659 var fig = new Roo.htmleditor.BlockFigure({
662 fig.updateElement(img); // replace it..
668 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
669 if (this.enableBlocks) {
670 Roo.htmleditor.Block.initAll(this.doc.body);
676 // default behaveiour should be our local cleanup paste? (optional?)
677 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
678 //this.owner.fireEvent('paste', e, v);
681 onDestroy : function(){
687 //for (var i =0; i < this.toolbars.length;i++) {
688 // // fixme - ask toolbars for heights?
689 // this.toolbars[i].onDestroy();
692 //this.wrap.dom.innerHTML = '';
693 //this.wrap.remove();
698 onFirstFocus : function(){
701 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
703 this.activated = true;
706 if(Roo.isGecko){ // prevent silly gecko errors
708 var s = this.win.getSelection();
709 if(!s.focusNode || s.focusNode.nodeType != 3){
710 var r = s.getRangeAt(0);
711 r.selectNodeContents((this.doc.body || this.doc.documentElement));
716 this.execCmd('useCSS', true);
717 this.execCmd('styleWithCSS', false);
720 this.owner.fireEvent('activate', this);
724 adjustFont: function(btn){
725 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
726 //if(Roo.isSafari){ // safari
729 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
730 if(Roo.isSafari){ // safari
731 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
732 v = (v < 10) ? 10 : v;
733 v = (v > 48) ? 48 : v;
734 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
739 v = Math.max(1, v+adjust);
741 this.execCmd('FontSize', v );
744 onEditorEvent : function(e)
748 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
749 return; // we do not handle this.. (undo manager does..)
751 // in theory this detects if the last element is not a br, then we try and do that.
752 // its so clicking in space at bottom triggers adding a br and moving the cursor.
754 e.target.nodeName == 'BODY' &&
755 e.type == "mouseup" &&
756 this.doc.body.lastChild
758 var lc = this.doc.body.lastChild;
759 // gtx-trans is google translate plugin adding crap.
760 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
761 lc = lc.previousSibling;
763 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
764 // if last element is <BR> - then dont do anything.
766 var ns = this.doc.createElement('br');
767 this.doc.body.appendChild(ns);
768 range = this.doc.createRange();
769 range.setStartAfter(ns);
770 range.collapse(true);
771 var sel = this.win.getSelection();
772 sel.removeAllRanges();
779 this.fireEditorEvent(e);
780 // this.updateToolbar();
781 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
784 fireEditorEvent: function(e)
786 this.owner.fireEvent('editorevent', this, e);
789 insertTag : function(tg)
791 // could be a bit smarter... -> wrap the current selected tRoo..
792 if (tg.toLowerCase() == 'span' ||
793 tg.toLowerCase() == 'code' ||
794 tg.toLowerCase() == 'sup' ||
795 tg.toLowerCase() == 'sub'
798 range = this.createRange(this.getSelection());
799 var wrappingNode = this.doc.createElement(tg.toLowerCase());
800 wrappingNode.appendChild(range.extractContents());
801 range.insertNode(wrappingNode);
808 this.execCmd("formatblock", tg);
809 this.undoManager.addEvent();
812 insertText : function(txt)
816 var range = this.createRange();
817 range.deleteContents();
818 //alert(Sender.getAttribute('label'));
820 range.insertNode(this.doc.createTextNode(txt));
821 this.undoManager.addEvent();
827 * Executes a Midas editor command on the editor document and performs necessary focus and
828 * toolbar updates. <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 relayCmd : function(cmd, value)
838 case 'justifycenter':
839 // if we are in a cell, then we will adjust the
840 var n = this.getParentElement();
841 var td = n.closest('td');
843 var bl = Roo.htmleditor.Block.factory(td);
844 bl.textAlign = cmd.replace('justify','');
846 this.owner.fireEvent('editorevent', this);
849 this.execCmd('styleWithCSS', true); //
853 // if there is no selection, then we insert, and set the curson inside it..
854 this.execCmd('styleWithCSS', false);
864 this.execCmd(cmd, value);
865 this.owner.fireEvent('editorevent', this);
866 //this.updateToolbar();
867 this.owner.deferFocus();
871 * Executes a Midas editor command directly on the editor document.
872 * For visual commands, you should use {@link #relayCmd} instead.
873 * <b>This should only be called after the editor is initialized.</b>
874 * @param {String} cmd The Midas command
875 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
877 execCmd : function(cmd, value){
878 this.doc.execCommand(cmd, false, value === undefined ? null : value);
885 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
887 * @param {String} text | dom node..
889 insertAtCursor : function(text)
896 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
900 // from jquery ui (MIT licenced)
904 if (win.getSelection && win.getSelection().getRangeAt) {
906 // delete the existing?
908 this.createRange(this.getSelection()).deleteContents();
909 range = win.getSelection().getRangeAt(0);
910 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
911 range.insertNode(node);
912 range = range.cloneRange();
913 range.collapse(false);
915 win.getSelection().removeAllRanges();
916 win.getSelection().addRange(range);
920 } else if (win.document.selection && win.document.selection.createRange) {
921 // no firefox support
922 var txt = typeof(text) == 'string' ? text : text.outerHTML;
923 win.document.selection.createRange().pasteHTML(txt);
926 // no firefox support
927 var txt = typeof(text) == 'string' ? text : text.outerHTML;
928 this.execCmd('InsertHTML', txt);
936 mozKeyPress : function(e){
938 var c = e.getCharCode(), cmd;
941 c = String.fromCharCode(c).toLowerCase();
955 // this.cleanUpPaste.defer(100, this);
973 fixKeys : function(){ // load time branching for fastest keydown performance
978 var k = e.getKey(), r;
981 r = this.doc.selection.createRange();
984 r.pasteHTML('    ');
989 /// this is handled by Roo.htmleditor.KeyEnter
992 r = this.doc.selection.createRange();
994 var target = r.parentElement();
995 if(!target || target.tagName.toLowerCase() != 'li'){
997 r.pasteHTML('<br/>');
1004 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1005 // this.cleanUpPaste.defer(100, this);
1011 }else if(Roo.isOpera){
1017 this.execCmd('InsertHTML','    ');
1021 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1022 // this.cleanUpPaste.defer(100, this);
1027 }else if(Roo.isSafari){
1033 this.execCmd('InsertText','\t');
1037 this.mozKeyPress(e);
1039 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1040 // this.cleanUpPaste.defer(100, this);
1048 getAllAncestors: function()
1050 var p = this.getSelectedNode();
1053 a.push(p); // push blank onto stack..
1054 p = this.getParentElement();
1058 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1062 a.push(this.doc.body);
1066 lastSelNode : false,
1069 getSelection : function()
1071 this.assignDocWin();
1072 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1076 * @param {DomElement} node the node to select
1078 selectNode : function(node, collapse)
1080 var nodeRange = node.ownerDocument.createRange();
1082 nodeRange.selectNode(node);
1084 nodeRange.selectNodeContents(node);
1086 if (collapse === true) {
1087 nodeRange.collapse(true);
1090 var s = this.win.getSelection();
1091 s.removeAllRanges();
1092 s.addRange(nodeRange);
1095 getSelectedNode: function()
1097 // this may only work on Gecko!!!
1099 // should we cache this!!!!
1103 var range = this.createRange(this.getSelection()).cloneRange();
1106 var parent = range.parentElement();
1108 var testRange = range.duplicate();
1109 testRange.moveToElementText(parent);
1110 if (testRange.inRange(range)) {
1113 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1116 parent = parent.parentElement;
1121 // is ancestor a text element.
1122 var ac = range.commonAncestorContainer;
1123 if (ac.nodeType == 3) {
1127 var ar = ac.childNodes;
1130 var other_nodes = [];
1131 var has_other_nodes = false;
1132 for (var i=0;i<ar.length;i++) {
1133 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1136 // fullly contained node.
1138 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1143 // probably selected..
1144 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1145 other_nodes.push(ar[i]);
1149 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1154 has_other_nodes = true;
1156 if (!nodes.length && other_nodes.length) {
1159 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1167 createRange: function(sel)
1169 // this has strange effects when using with
1170 // top toolbar - not sure if it's a great idea.
1171 //this.editor.contentWindow.focus();
1172 if (typeof sel != "undefined") {
1174 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1176 return this.doc.createRange();
1179 return this.doc.createRange();
1182 getParentElement: function()
1185 this.assignDocWin();
1186 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1188 var range = this.createRange(sel);
1191 var p = range.commonAncestorContainer;
1192 while (p.nodeType == 3) { // text node
1203 * Range intersection.. the hard stuff...
1207 * [ -- selected range --- ]
1211 * if end is before start or hits it. fail.
1212 * if start is after end or hits it fail.
1214 * if either hits (but other is outside. - then it's not
1220 // @see http://www.thismuchiknow.co.uk/?p=64.
1221 rangeIntersectsNode : function(range, node)
1223 var nodeRange = node.ownerDocument.createRange();
1225 nodeRange.selectNode(node);
1227 nodeRange.selectNodeContents(node);
1230 var rangeStartRange = range.cloneRange();
1231 rangeStartRange.collapse(true);
1233 var rangeEndRange = range.cloneRange();
1234 rangeEndRange.collapse(false);
1236 var nodeStartRange = nodeRange.cloneRange();
1237 nodeStartRange.collapse(true);
1239 var nodeEndRange = nodeRange.cloneRange();
1240 nodeEndRange.collapse(false);
1242 return rangeStartRange.compareBoundaryPoints(
1243 Range.START_TO_START, nodeEndRange) == -1 &&
1244 rangeEndRange.compareBoundaryPoints(
1245 Range.START_TO_START, nodeStartRange) == 1;
1249 rangeCompareNode : function(range, node)
1251 var nodeRange = node.ownerDocument.createRange();
1253 nodeRange.selectNode(node);
1255 nodeRange.selectNodeContents(node);
1259 range.collapse(true);
1261 nodeRange.collapse(true);
1263 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1264 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1266 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1268 var nodeIsBefore = ss == 1;
1269 var nodeIsAfter = ee == -1;
1271 if (nodeIsBefore && nodeIsAfter) {
1274 if (!nodeIsBefore && nodeIsAfter) {
1275 return 1; //right trailed.
1278 if (nodeIsBefore && !nodeIsAfter) {
1279 return 2; // left trailed.
1285 cleanWordChars : function(input) {// change the chars to hex code
1288 [ 8211, "–" ],
1289 [ 8212, "—" ],
1298 Roo.each(swapCodes, function(sw) {
1299 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1301 output = output.replace(swapper, sw[1]);
1311 cleanUpChild : function (node)
1314 new Roo.htmleditor.FilterComment({node : node});
1315 new Roo.htmleditor.FilterAttributes({
1317 attrib_black : this.ablack,
1318 attrib_clean : this.aclean,
1319 style_white : this.cwhite,
1320 style_black : this.cblack
1322 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1323 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1329 * Clean up MS wordisms...
1330 * @deprecated - use filter directly
1332 cleanWord : function(node)
1334 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1335 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1342 * @deprecated - use filters
1344 cleanTableWidths : function(node)
1346 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1353 applyBlacklists : function()
1355 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1356 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1358 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1359 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1360 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1364 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1365 if (b.indexOf(tag) > -1) {
1368 this.white.push(tag);
1372 Roo.each(w, function(tag) {
1373 if (b.indexOf(tag) > -1) {
1376 if (this.white.indexOf(tag) > -1) {
1379 this.white.push(tag);
1384 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1385 if (w.indexOf(tag) > -1) {
1388 this.black.push(tag);
1392 Roo.each(b, function(tag) {
1393 if (w.indexOf(tag) > -1) {
1396 if (this.black.indexOf(tag) > -1) {
1399 this.black.push(tag);
1404 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1405 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1409 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1410 if (b.indexOf(tag) > -1) {
1413 this.cwhite.push(tag);
1417 Roo.each(w, function(tag) {
1418 if (b.indexOf(tag) > -1) {
1421 if (this.cwhite.indexOf(tag) > -1) {
1424 this.cwhite.push(tag);
1429 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1430 if (w.indexOf(tag) > -1) {
1433 this.cblack.push(tag);
1437 Roo.each(b, function(tag) {
1438 if (w.indexOf(tag) > -1) {
1441 if (this.cblack.indexOf(tag) > -1) {
1444 this.cblack.push(tag);
1449 setStylesheets : function(stylesheets)
1451 if(typeof(stylesheets) == 'string'){
1452 Roo.get(this.iframe.contentDocument.head).createChild({
1463 Roo.each(stylesheets, function(s) {
1468 Roo.get(_this.iframe.contentDocument.head).createChild({
1480 updateLanguage : function()
1482 if (!this.iframe || !this.iframe.contentDocument) {
1485 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1489 removeStylesheets : function()
1493 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1498 setStyle : function(style)
1500 Roo.get(this.iframe.contentDocument.head).createChild({
1509 // hide stuff that is not compatible
1527 * @cfg {String} fieldClass @hide
1530 * @cfg {String} focusClass @hide
1533 * @cfg {String} autoCreate @hide
1536 * @cfg {String} inputType @hide
1539 * @cfg {String} invalidClass @hide
1542 * @cfg {String} invalidText @hide
1545 * @cfg {String} msgFx @hide
1548 * @cfg {String} validateOnBlur @hide
1552 Roo.HtmlEditorCore.white = [
1553 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1555 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1556 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1557 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1558 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1559 'TABLE', 'UL', 'XMP',
1561 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1564 'DIR', 'MENU', 'OL', 'UL', 'DL',
1570 Roo.HtmlEditorCore.black = [
1571 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1573 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1574 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1575 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1576 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1577 //'FONT' // CLEAN LATER..
1578 'COLGROUP', 'COL' // messy tables.
1582 Roo.HtmlEditorCore.clean = [ // ?? needed???
1583 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1585 Roo.HtmlEditorCore.tag_remove = [
1590 Roo.HtmlEditorCore.ablack = [
1594 Roo.HtmlEditorCore.aclean = [
1595 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1599 Roo.HtmlEditorCore.pwhite= [
1600 'http', 'https', 'mailto'
1603 // white listed style attributes.
1604 Roo.HtmlEditorCore.cwhite= [
1605 // 'text-align', /// default is to allow most things..
1611 // black listed style attributes.
1612 Roo.HtmlEditorCore.cblack= [
1613 // 'font-size' -- this can be set by the project