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 tidy = new Roo.htmleditor.TidySerializer({
384 var html = tidy.serialize(div);
388 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
389 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
391 html = '<div style="'+m[0]+'">' + html + '</div>';
394 html = this.cleanHtml(html);
395 // fix up the special chars.. normaly like back quotes in word...
396 // however we do not want to do this with chinese..
397 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
399 var cc = match.charCodeAt();
401 // Get the character value, handling surrogate pairs
402 if (match.length == 2) {
403 // It's a surrogate pair, calculate the Unicode code point
404 var high = match.charCodeAt(0) - 0xD800;
405 var low = match.charCodeAt(1) - 0xDC00;
406 cc = (high * 0x400) + low + 0x10000;
408 (cc >= 0x4E00 && cc < 0xA000 ) ||
409 (cc >= 0x3400 && cc < 0x4E00 ) ||
410 (cc >= 0xf900 && cc < 0xfb00 )
415 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
416 return "&#" + cc + ";";
423 if(this.owner.fireEvent('beforesync', this, html) !== false){
424 this.el.dom.value = html;
425 this.owner.fireEvent('sync', this, html);
431 * TEXTAREA -> EDITABLE
432 * Protected method that will not generally be called directly. Pushes the value of the textarea
433 * into the iframe editor.
435 pushValue : function()
437 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
438 if(this.initialized){
439 var v = this.el.dom.value.trim();
442 if(this.owner.fireEvent('beforepush', this, v) !== false){
443 var d = (this.doc.body || this.doc.documentElement);
446 this.el.dom.value = d.innerHTML;
447 this.owner.fireEvent('push', this, v);
449 if (this.autoClean) {
450 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
451 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
453 if (this.enableBlocks) {
454 Roo.htmleditor.Block.initAll(this.doc.body);
457 this.updateLanguage();
459 var lc = this.doc.body.lastChild;
460 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
461 // add an extra line at the end.
462 this.doc.body.appendChild(this.doc.createElement('br'));
470 deferFocus : function(){
471 this.focus.defer(10, this);
476 if(this.win && !this.sourceEditMode){
483 assignDocWin: function()
485 var iframe = this.iframe;
488 this.doc = iframe.contentWindow.document;
489 this.win = iframe.contentWindow;
491 // if (!Roo.get(this.frameId)) {
494 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
495 // this.win = Roo.get(this.frameId).dom.contentWindow;
497 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
501 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
502 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
507 initEditor : function(){
508 //console.log("INIT EDITOR");
513 this.doc.designMode="on";
515 this.doc.write(this.getDocMarkup());
518 var dbody = (this.doc.body || this.doc.documentElement);
519 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
520 // this copies styles from the containing element into thsi one..
521 // not sure why we need all of this..
522 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
524 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
525 //ss['background-attachment'] = 'fixed'; // w3c
526 dbody.bgProperties = 'fixed'; // ie
527 dbody.setAttribute("translate", "no");
529 //Roo.DomHelper.applyStyles(dbody, ss);
530 Roo.EventManager.on(this.doc, {
532 'mouseup': this.onEditorEvent,
533 'dblclick': this.onEditorEvent,
534 'click': this.onEditorEvent,
535 'keyup': this.onEditorEvent,
540 Roo.EventManager.on(this.doc, {
541 'paste': this.onPasteEvent,
545 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
548 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
549 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
551 this.initialized = true;
554 // initialize special key events - enter
555 new Roo.htmleditor.KeyEnter({core : this});
559 this.owner.fireEvent('initialize', this);
562 // this is to prevent a href clicks resulting in a redirect?
564 onPasteEvent : function(e,v)
566 // I think we better assume paste is going to be a dirty load of rubish from word..
568 // even pasting into a 'email version' of this widget will have to clean up that mess.
569 var cd = (e.browserEvent.clipboardData || window.clipboardData);
571 // check what type of paste - if it's an image, then handle it differently.
572 if (cd.files && cd.files.length > 0) {
574 var urlAPI = (window.createObjectURL && window) ||
575 (window.URL && URL.revokeObjectURL && URL) ||
576 (window.webkitURL && webkitURL);
578 var url = urlAPI.createObjectURL( cd.files[0]);
579 this.insertAtCursor('<img src=" + url + ">');
582 if (cd.types.indexOf('text/html') < 0 ) {
586 var html = cd.getData('text/html'); // clipboard event
587 if (cd.types.indexOf('text/rtf') > -1) {
588 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
589 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
594 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
595 .map(function(g) { return g.toDataURL(); })
596 .filter(function(g) { return g != 'about:blank'; });
599 html = this.cleanWordChars(html);
601 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
604 var sn = this.getParentElement();
605 // check if d contains a table, and prevent nesting??
606 //Roo.log(d.getElementsByTagName('table'));
608 //Roo.log(sn.closest('table'));
609 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
611 this.insertAtCursor("You can not nest tables");
612 //Roo.log("prevent?"); // fixme -
616 if (images.length > 0) {
617 // if they mix thse, then it's going to be messy...
618 var imgs = Array.from(d.getElementsByTagName('img'));
619 imgs.concat(Array.from(document.getElementsByTagName('v:imagedata'))); // not sure if we need array from here.
622 Roo.each(imgs, function(img, i) {
623 img.setAttribute('src', images[i]);
626 if (this.autoClean) {
627 new Roo.htmleditor.FilterWord({ node : d });
629 new Roo.htmleditor.FilterStyleToTag({ node : d });
630 new Roo.htmleditor.FilterAttributes({
632 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width'],
633 attrib_clean : ['href', 'src' ]
635 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
637 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
638 new Roo.htmleditor.FilterParagraph({ node : d });
639 new Roo.htmleditor.FilterSpan({ node : d });
640 new Roo.htmleditor.FilterLongBr({ node : d });
641 new Roo.htmleditor.FilterComment({ node : d });
645 if (this.enableBlocks) {
647 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
648 if (img.closest('figure')) { // assume!! that it's aready
651 var fig = new Roo.htmleditor.BlockFigure({
654 fig.updateElement(img); // replace it..
660 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
661 if (this.enableBlocks) {
662 Roo.htmleditor.Block.initAll(this.doc.body);
668 // default behaveiour should be our local cleanup paste? (optional?)
669 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
670 //this.owner.fireEvent('paste', e, v);
673 onDestroy : function(){
679 //for (var i =0; i < this.toolbars.length;i++) {
680 // // fixme - ask toolbars for heights?
681 // this.toolbars[i].onDestroy();
684 //this.wrap.dom.innerHTML = '';
685 //this.wrap.remove();
690 onFirstFocus : function(){
693 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
695 this.activated = true;
698 if(Roo.isGecko){ // prevent silly gecko errors
700 var s = this.win.getSelection();
701 if(!s.focusNode || s.focusNode.nodeType != 3){
702 var r = s.getRangeAt(0);
703 r.selectNodeContents((this.doc.body || this.doc.documentElement));
708 this.execCmd('useCSS', true);
709 this.execCmd('styleWithCSS', false);
712 this.owner.fireEvent('activate', this);
716 adjustFont: function(btn){
717 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
718 //if(Roo.isSafari){ // safari
721 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
722 if(Roo.isSafari){ // safari
723 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
724 v = (v < 10) ? 10 : v;
725 v = (v > 48) ? 48 : v;
726 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
731 v = Math.max(1, v+adjust);
733 this.execCmd('FontSize', v );
736 onEditorEvent : function(e)
740 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
741 return; // we do not handle this.. (undo manager does..)
743 // in theory this detects if the last element is not a br, then we try and do that.
744 // its so clicking in space at bottom triggers adding a br and moving the cursor.
746 e.target.nodeName == 'BODY' &&
747 e.type == "mouseup" &&
748 this.doc.body.lastChild
750 var lc = this.doc.body.lastChild;
751 // gtx-trans is google translate plugin adding crap.
752 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
753 lc = lc.previousSibling;
755 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
756 // if last element is <BR> - then dont do anything.
758 var ns = this.doc.createElement('br');
759 this.doc.body.appendChild(ns);
760 range = this.doc.createRange();
761 range.setStartAfter(ns);
762 range.collapse(true);
763 var sel = this.win.getSelection();
764 sel.removeAllRanges();
771 this.fireEditorEvent(e);
772 // this.updateToolbar();
773 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
776 fireEditorEvent: function(e)
778 this.owner.fireEvent('editorevent', this, e);
781 insertTag : function(tg)
783 // could be a bit smarter... -> wrap the current selected tRoo..
784 if (tg.toLowerCase() == 'span' ||
785 tg.toLowerCase() == 'code' ||
786 tg.toLowerCase() == 'sup' ||
787 tg.toLowerCase() == 'sub'
790 range = this.createRange(this.getSelection());
791 var wrappingNode = this.doc.createElement(tg.toLowerCase());
792 wrappingNode.appendChild(range.extractContents());
793 range.insertNode(wrappingNode);
800 this.execCmd("formatblock", tg);
801 this.undoManager.addEvent();
804 insertText : function(txt)
808 var range = this.createRange();
809 range.deleteContents();
810 //alert(Sender.getAttribute('label'));
812 range.insertNode(this.doc.createTextNode(txt));
813 this.undoManager.addEvent();
819 * Executes a Midas editor command on the editor document and performs necessary focus and
820 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
821 * @param {String} cmd The Midas command
822 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
824 relayCmd : function(cmd, value)
830 case 'justifycenter':
831 // if we are in a cell, then we will adjust the
832 var n = this.getParentElement();
833 var td = n.closest('td');
835 var bl = Roo.htmleditor.Block.factory(td);
836 bl.textAlign = cmd.replace('justify','');
838 this.owner.fireEvent('editorevent', this);
841 this.execCmd('styleWithCSS', true); //
845 // if there is no selection, then we insert, and set the curson inside it..
846 this.execCmd('styleWithCSS', false);
856 this.execCmd(cmd, value);
857 this.owner.fireEvent('editorevent', this);
858 //this.updateToolbar();
859 this.owner.deferFocus();
863 * Executes a Midas editor command directly on the editor document.
864 * For visual commands, you should use {@link #relayCmd} instead.
865 * <b>This should only be called after the editor is initialized.</b>
866 * @param {String} cmd The Midas command
867 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
869 execCmd : function(cmd, value){
870 this.doc.execCommand(cmd, false, value === undefined ? null : value);
877 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
879 * @param {String} text | dom node..
881 insertAtCursor : function(text)
888 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
892 // from jquery ui (MIT licenced)
896 if (win.getSelection && win.getSelection().getRangeAt) {
898 // delete the existing?
900 this.createRange(this.getSelection()).deleteContents();
901 range = win.getSelection().getRangeAt(0);
902 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
903 range.insertNode(node);
904 range = range.cloneRange();
905 range.collapse(false);
907 win.getSelection().removeAllRanges();
908 win.getSelection().addRange(range);
912 } else if (win.document.selection && win.document.selection.createRange) {
913 // no firefox support
914 var txt = typeof(text) == 'string' ? text : text.outerHTML;
915 win.document.selection.createRange().pasteHTML(txt);
918 // no firefox support
919 var txt = typeof(text) == 'string' ? text : text.outerHTML;
920 this.execCmd('InsertHTML', txt);
928 mozKeyPress : function(e){
930 var c = e.getCharCode(), cmd;
933 c = String.fromCharCode(c).toLowerCase();
947 // this.cleanUpPaste.defer(100, this);
965 fixKeys : function(){ // load time branching for fastest keydown performance
970 var k = e.getKey(), r;
973 r = this.doc.selection.createRange();
976 r.pasteHTML('    ');
981 /// this is handled by Roo.htmleditor.KeyEnter
984 r = this.doc.selection.createRange();
986 var target = r.parentElement();
987 if(!target || target.tagName.toLowerCase() != 'li'){
989 r.pasteHTML('<br/>');
996 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
997 // this.cleanUpPaste.defer(100, this);
1003 }else if(Roo.isOpera){
1009 this.execCmd('InsertHTML','    ');
1013 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1014 // this.cleanUpPaste.defer(100, this);
1019 }else if(Roo.isSafari){
1025 this.execCmd('InsertText','\t');
1029 this.mozKeyPress(e);
1031 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1032 // this.cleanUpPaste.defer(100, this);
1040 getAllAncestors: function()
1042 var p = this.getSelectedNode();
1045 a.push(p); // push blank onto stack..
1046 p = this.getParentElement();
1050 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1054 a.push(this.doc.body);
1058 lastSelNode : false,
1061 getSelection : function()
1063 this.assignDocWin();
1064 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1068 * @param {DomElement} node the node to select
1070 selectNode : function(node, collapse)
1072 var nodeRange = node.ownerDocument.createRange();
1074 nodeRange.selectNode(node);
1076 nodeRange.selectNodeContents(node);
1078 if (collapse === true) {
1079 nodeRange.collapse(true);
1082 var s = this.win.getSelection();
1083 s.removeAllRanges();
1084 s.addRange(nodeRange);
1087 getSelectedNode: function()
1089 // this may only work on Gecko!!!
1091 // should we cache this!!!!
1095 var range = this.createRange(this.getSelection()).cloneRange();
1098 var parent = range.parentElement();
1100 var testRange = range.duplicate();
1101 testRange.moveToElementText(parent);
1102 if (testRange.inRange(range)) {
1105 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1108 parent = parent.parentElement;
1113 // is ancestor a text element.
1114 var ac = range.commonAncestorContainer;
1115 if (ac.nodeType == 3) {
1119 var ar = ac.childNodes;
1122 var other_nodes = [];
1123 var has_other_nodes = false;
1124 for (var i=0;i<ar.length;i++) {
1125 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1128 // fullly contained node.
1130 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1135 // probably selected..
1136 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1137 other_nodes.push(ar[i]);
1141 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1146 has_other_nodes = true;
1148 if (!nodes.length && other_nodes.length) {
1151 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1159 createRange: function(sel)
1161 // this has strange effects when using with
1162 // top toolbar - not sure if it's a great idea.
1163 //this.editor.contentWindow.focus();
1164 if (typeof sel != "undefined") {
1166 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1168 return this.doc.createRange();
1171 return this.doc.createRange();
1174 getParentElement: function()
1177 this.assignDocWin();
1178 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1180 var range = this.createRange(sel);
1183 var p = range.commonAncestorContainer;
1184 while (p.nodeType == 3) { // text node
1195 * Range intersection.. the hard stuff...
1199 * [ -- selected range --- ]
1203 * if end is before start or hits it. fail.
1204 * if start is after end or hits it fail.
1206 * if either hits (but other is outside. - then it's not
1212 // @see http://www.thismuchiknow.co.uk/?p=64.
1213 rangeIntersectsNode : function(range, node)
1215 var nodeRange = node.ownerDocument.createRange();
1217 nodeRange.selectNode(node);
1219 nodeRange.selectNodeContents(node);
1222 var rangeStartRange = range.cloneRange();
1223 rangeStartRange.collapse(true);
1225 var rangeEndRange = range.cloneRange();
1226 rangeEndRange.collapse(false);
1228 var nodeStartRange = nodeRange.cloneRange();
1229 nodeStartRange.collapse(true);
1231 var nodeEndRange = nodeRange.cloneRange();
1232 nodeEndRange.collapse(false);
1234 return rangeStartRange.compareBoundaryPoints(
1235 Range.START_TO_START, nodeEndRange) == -1 &&
1236 rangeEndRange.compareBoundaryPoints(
1237 Range.START_TO_START, nodeStartRange) == 1;
1241 rangeCompareNode : function(range, node)
1243 var nodeRange = node.ownerDocument.createRange();
1245 nodeRange.selectNode(node);
1247 nodeRange.selectNodeContents(node);
1251 range.collapse(true);
1253 nodeRange.collapse(true);
1255 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1256 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1258 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1260 var nodeIsBefore = ss == 1;
1261 var nodeIsAfter = ee == -1;
1263 if (nodeIsBefore && nodeIsAfter) {
1266 if (!nodeIsBefore && nodeIsAfter) {
1267 return 1; //right trailed.
1270 if (nodeIsBefore && !nodeIsAfter) {
1271 return 2; // left trailed.
1277 cleanWordChars : function(input) {// change the chars to hex code
1280 [ 8211, "–" ],
1281 [ 8212, "—" ],
1290 Roo.each(swapCodes, function(sw) {
1291 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1293 output = output.replace(swapper, sw[1]);
1303 cleanUpChild : function (node)
1306 new Roo.htmleditor.FilterComment({node : node});
1307 new Roo.htmleditor.FilterAttributes({
1309 attrib_black : this.ablack,
1310 attrib_clean : this.aclean,
1311 style_white : this.cwhite,
1312 style_black : this.cblack
1314 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1315 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1321 * Clean up MS wordisms...
1322 * @deprecated - use filter directly
1324 cleanWord : function(node)
1326 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1327 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1334 * @deprecated - use filters
1336 cleanTableWidths : function(node)
1338 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1345 applyBlacklists : function()
1347 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1348 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1350 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1351 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1352 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1356 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1357 if (b.indexOf(tag) > -1) {
1360 this.white.push(tag);
1364 Roo.each(w, function(tag) {
1365 if (b.indexOf(tag) > -1) {
1368 if (this.white.indexOf(tag) > -1) {
1371 this.white.push(tag);
1376 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1377 if (w.indexOf(tag) > -1) {
1380 this.black.push(tag);
1384 Roo.each(b, function(tag) {
1385 if (w.indexOf(tag) > -1) {
1388 if (this.black.indexOf(tag) > -1) {
1391 this.black.push(tag);
1396 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1397 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1401 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1402 if (b.indexOf(tag) > -1) {
1405 this.cwhite.push(tag);
1409 Roo.each(w, function(tag) {
1410 if (b.indexOf(tag) > -1) {
1413 if (this.cwhite.indexOf(tag) > -1) {
1416 this.cwhite.push(tag);
1421 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1422 if (w.indexOf(tag) > -1) {
1425 this.cblack.push(tag);
1429 Roo.each(b, function(tag) {
1430 if (w.indexOf(tag) > -1) {
1433 if (this.cblack.indexOf(tag) > -1) {
1436 this.cblack.push(tag);
1441 setStylesheets : function(stylesheets)
1443 if(typeof(stylesheets) == 'string'){
1444 Roo.get(this.iframe.contentDocument.head).createChild({
1455 Roo.each(stylesheets, function(s) {
1460 Roo.get(_this.iframe.contentDocument.head).createChild({
1472 updateLanguage : function()
1474 if (!this.iframe || !this.iframe.contentDocument) {
1477 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1481 removeStylesheets : function()
1485 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1490 setStyle : function(style)
1492 Roo.get(this.iframe.contentDocument.head).createChild({
1501 // hide stuff that is not compatible
1519 * @cfg {String} fieldClass @hide
1522 * @cfg {String} focusClass @hide
1525 * @cfg {String} autoCreate @hide
1528 * @cfg {String} inputType @hide
1531 * @cfg {String} invalidClass @hide
1534 * @cfg {String} invalidText @hide
1537 * @cfg {String} msgFx @hide
1540 * @cfg {String} validateOnBlur @hide
1544 Roo.HtmlEditorCore.white = [
1545 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1547 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1548 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1549 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1550 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1551 'TABLE', 'UL', 'XMP',
1553 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1556 'DIR', 'MENU', 'OL', 'UL', 'DL',
1562 Roo.HtmlEditorCore.black = [
1563 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1565 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1566 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1567 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1568 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1569 //'FONT' // CLEAN LATER..
1570 'COLGROUP', 'COL' // messy tables.
1574 Roo.HtmlEditorCore.clean = [ // ?? needed???
1575 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1577 Roo.HtmlEditorCore.tag_remove = [
1582 Roo.HtmlEditorCore.ablack = [
1586 Roo.HtmlEditorCore.aclean = [
1587 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1591 Roo.HtmlEditorCore.pwhite= [
1592 'http', 'https', 'mailto'
1595 // white listed style attributes.
1596 Roo.HtmlEditorCore.cwhite= [
1597 // 'text-align', /// default is to allow most things..
1603 // black listed style attributes.
1604 Roo.HtmlEditorCore.cblack= [
1605 // 'font-size' -- this can be set by the project