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 Roo.each(d.getElementsByTagName('img'), function(img, i) {
618 img.setAttribute('src', images[i]);
621 if (this.autoClean) {
622 new Roo.htmleditor.FilterStyleToTag({ node : d });
623 new Roo.htmleditor.FilterAttributes({
625 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width'],
626 attrib_clean : ['href', 'src' ]
628 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
630 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', 'O:P' ]} );
631 new Roo.htmleditor.FilterParagraph({ node : d });
632 new Roo.htmleditor.FilterSpan({ node : d });
633 new Roo.htmleditor.FilterLongBr({ node : d });
634 new Roo.htmleditor.FilterComment({ node : d });
638 if (this.enableBlocks) {
640 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
641 if (img.closest('figure')) { // assume!! that it's aready
644 var fig = new Roo.htmleditor.BlockFigure({
647 fig.updateElement(img); // replace it..
653 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
654 if (this.enableBlocks) {
655 Roo.htmleditor.Block.initAll(this.doc.body);
659 new Roo.htmleditor.FilterFileWarning({ node : d });
661 Roo.MessageBox.alert("Invalid URLS in content", "The pasted Content contains file:// URLS - you probably want to check all the links in this file");
666 // default behaveiour should be our local cleanup paste? (optional?)
667 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
668 //this.owner.fireEvent('paste', e, v);
671 onDestroy : function(){
677 //for (var i =0; i < this.toolbars.length;i++) {
678 // // fixme - ask toolbars for heights?
679 // this.toolbars[i].onDestroy();
682 //this.wrap.dom.innerHTML = '';
683 //this.wrap.remove();
688 onFirstFocus : function(){
691 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
693 this.activated = true;
696 if(Roo.isGecko){ // prevent silly gecko errors
698 var s = this.win.getSelection();
699 if(!s.focusNode || s.focusNode.nodeType != 3){
700 var r = s.getRangeAt(0);
701 r.selectNodeContents((this.doc.body || this.doc.documentElement));
706 this.execCmd('useCSS', true);
707 this.execCmd('styleWithCSS', false);
710 this.owner.fireEvent('activate', this);
714 adjustFont: function(btn){
715 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
716 //if(Roo.isSafari){ // safari
719 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
720 if(Roo.isSafari){ // safari
721 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
722 v = (v < 10) ? 10 : v;
723 v = (v > 48) ? 48 : v;
724 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
729 v = Math.max(1, v+adjust);
731 this.execCmd('FontSize', v );
734 onEditorEvent : function(e)
738 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
739 return; // we do not handle this.. (undo manager does..)
741 // in theory this detects if the last element is not a br, then we try and do that.
742 // its so clicking in space at bottom triggers adding a br and moving the cursor.
744 e.target.nodeName == 'BODY' &&
745 e.type == "mouseup" &&
746 this.doc.body.lastChild
748 var lc = this.doc.body.lastChild;
749 // gtx-trans is google translate plugin adding crap.
750 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
751 lc = lc.previousSibling;
753 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
754 // if last element is <BR> - then dont do anything.
756 var ns = this.doc.createElement('br');
757 this.doc.body.appendChild(ns);
758 range = this.doc.createRange();
759 range.setStartAfter(ns);
760 range.collapse(true);
761 var sel = this.win.getSelection();
762 sel.removeAllRanges();
769 this.fireEditorEvent(e);
770 // this.updateToolbar();
771 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
774 fireEditorEvent: function(e)
776 this.owner.fireEvent('editorevent', this, e);
779 insertTag : function(tg)
781 // could be a bit smarter... -> wrap the current selected tRoo..
782 if (tg.toLowerCase() == 'span' ||
783 tg.toLowerCase() == 'code' ||
784 tg.toLowerCase() == 'sup' ||
785 tg.toLowerCase() == 'sub'
788 range = this.createRange(this.getSelection());
789 var wrappingNode = this.doc.createElement(tg.toLowerCase());
790 wrappingNode.appendChild(range.extractContents());
791 range.insertNode(wrappingNode);
798 this.execCmd("formatblock", tg);
799 this.undoManager.addEvent();
802 insertText : function(txt)
806 var range = this.createRange();
807 range.deleteContents();
808 //alert(Sender.getAttribute('label'));
810 range.insertNode(this.doc.createTextNode(txt));
811 this.undoManager.addEvent();
817 * Executes a Midas editor command on the editor document and performs necessary focus and
818 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
819 * @param {String} cmd The Midas command
820 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
822 relayCmd : function(cmd, value)
828 case 'justifycenter':
829 // if we are in a cell, then we will adjust the
830 var n = this.getParentElement();
831 var td = n.closest('td');
833 var bl = Roo.htmleditor.Block.factory(td);
834 bl.textAlign = cmd.replace('justify','');
836 this.owner.fireEvent('editorevent', this);
839 this.execCmd('styleWithCSS', true); //
843 // if there is no selection, then we insert, and set the curson inside it..
844 this.execCmd('styleWithCSS', false);
854 this.execCmd(cmd, value);
855 this.owner.fireEvent('editorevent', this);
856 //this.updateToolbar();
857 this.owner.deferFocus();
861 * Executes a Midas editor command directly on the editor document.
862 * For visual commands, you should use {@link #relayCmd} instead.
863 * <b>This should only be called after the editor is initialized.</b>
864 * @param {String} cmd The Midas command
865 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
867 execCmd : function(cmd, value){
868 this.doc.execCommand(cmd, false, value === undefined ? null : value);
875 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
877 * @param {String} text | dom node..
879 insertAtCursor : function(text)
886 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
890 // from jquery ui (MIT licenced)
894 if (win.getSelection && win.getSelection().getRangeAt) {
896 // delete the existing?
898 this.createRange(this.getSelection()).deleteContents();
899 range = win.getSelection().getRangeAt(0);
900 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
901 range.insertNode(node);
902 range = range.cloneRange();
903 range.collapse(false);
905 win.getSelection().removeAllRanges();
906 win.getSelection().addRange(range);
910 } else if (win.document.selection && win.document.selection.createRange) {
911 // no firefox support
912 var txt = typeof(text) == 'string' ? text : text.outerHTML;
913 win.document.selection.createRange().pasteHTML(txt);
916 // no firefox support
917 var txt = typeof(text) == 'string' ? text : text.outerHTML;
918 this.execCmd('InsertHTML', txt);
926 mozKeyPress : function(e){
928 var c = e.getCharCode(), cmd;
931 c = String.fromCharCode(c).toLowerCase();
945 // this.cleanUpPaste.defer(100, this);
963 fixKeys : function(){ // load time branching for fastest keydown performance
968 var k = e.getKey(), r;
971 r = this.doc.selection.createRange();
974 r.pasteHTML('    ');
979 /// this is handled by Roo.htmleditor.KeyEnter
982 r = this.doc.selection.createRange();
984 var target = r.parentElement();
985 if(!target || target.tagName.toLowerCase() != 'li'){
987 r.pasteHTML('<br/>');
994 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
995 // this.cleanUpPaste.defer(100, this);
1001 }else if(Roo.isOpera){
1007 this.execCmd('InsertHTML','    ');
1011 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1012 // this.cleanUpPaste.defer(100, this);
1017 }else if(Roo.isSafari){
1023 this.execCmd('InsertText','\t');
1027 this.mozKeyPress(e);
1029 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1030 // this.cleanUpPaste.defer(100, this);
1038 getAllAncestors: function()
1040 var p = this.getSelectedNode();
1043 a.push(p); // push blank onto stack..
1044 p = this.getParentElement();
1048 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1052 a.push(this.doc.body);
1056 lastSelNode : false,
1059 getSelection : function()
1061 this.assignDocWin();
1062 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1066 * @param {DomElement} node the node to select
1068 selectNode : function(node, collapse)
1070 var nodeRange = node.ownerDocument.createRange();
1072 nodeRange.selectNode(node);
1074 nodeRange.selectNodeContents(node);
1076 if (collapse === true) {
1077 nodeRange.collapse(true);
1080 var s = this.win.getSelection();
1081 s.removeAllRanges();
1082 s.addRange(nodeRange);
1085 getSelectedNode: function()
1087 // this may only work on Gecko!!!
1089 // should we cache this!!!!
1093 var range = this.createRange(this.getSelection()).cloneRange();
1096 var parent = range.parentElement();
1098 var testRange = range.duplicate();
1099 testRange.moveToElementText(parent);
1100 if (testRange.inRange(range)) {
1103 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1106 parent = parent.parentElement;
1111 // is ancestor a text element.
1112 var ac = range.commonAncestorContainer;
1113 if (ac.nodeType == 3) {
1117 var ar = ac.childNodes;
1120 var other_nodes = [];
1121 var has_other_nodes = false;
1122 for (var i=0;i<ar.length;i++) {
1123 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1126 // fullly contained node.
1128 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1133 // probably selected..
1134 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1135 other_nodes.push(ar[i]);
1139 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1144 has_other_nodes = true;
1146 if (!nodes.length && other_nodes.length) {
1149 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1157 createRange: function(sel)
1159 // this has strange effects when using with
1160 // top toolbar - not sure if it's a great idea.
1161 //this.editor.contentWindow.focus();
1162 if (typeof sel != "undefined") {
1164 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1166 return this.doc.createRange();
1169 return this.doc.createRange();
1172 getParentElement: function()
1175 this.assignDocWin();
1176 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1178 var range = this.createRange(sel);
1181 var p = range.commonAncestorContainer;
1182 while (p.nodeType == 3) { // text node
1193 * Range intersection.. the hard stuff...
1197 * [ -- selected range --- ]
1201 * if end is before start or hits it. fail.
1202 * if start is after end or hits it fail.
1204 * if either hits (but other is outside. - then it's not
1210 // @see http://www.thismuchiknow.co.uk/?p=64.
1211 rangeIntersectsNode : function(range, node)
1213 var nodeRange = node.ownerDocument.createRange();
1215 nodeRange.selectNode(node);
1217 nodeRange.selectNodeContents(node);
1220 var rangeStartRange = range.cloneRange();
1221 rangeStartRange.collapse(true);
1223 var rangeEndRange = range.cloneRange();
1224 rangeEndRange.collapse(false);
1226 var nodeStartRange = nodeRange.cloneRange();
1227 nodeStartRange.collapse(true);
1229 var nodeEndRange = nodeRange.cloneRange();
1230 nodeEndRange.collapse(false);
1232 return rangeStartRange.compareBoundaryPoints(
1233 Range.START_TO_START, nodeEndRange) == -1 &&
1234 rangeEndRange.compareBoundaryPoints(
1235 Range.START_TO_START, nodeStartRange) == 1;
1239 rangeCompareNode : function(range, node)
1241 var nodeRange = node.ownerDocument.createRange();
1243 nodeRange.selectNode(node);
1245 nodeRange.selectNodeContents(node);
1249 range.collapse(true);
1251 nodeRange.collapse(true);
1253 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1254 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1256 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1258 var nodeIsBefore = ss == 1;
1259 var nodeIsAfter = ee == -1;
1261 if (nodeIsBefore && nodeIsAfter) {
1264 if (!nodeIsBefore && nodeIsAfter) {
1265 return 1; //right trailed.
1268 if (nodeIsBefore && !nodeIsAfter) {
1269 return 2; // left trailed.
1275 cleanWordChars : function(input) {// change the chars to hex code
1278 [ 8211, "–" ],
1279 [ 8212, "—" ],
1288 Roo.each(swapCodes, function(sw) {
1289 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1291 output = output.replace(swapper, sw[1]);
1301 cleanUpChild : function (node)
1304 new Roo.htmleditor.FilterComment({node : node});
1305 new Roo.htmleditor.FilterAttributes({
1307 attrib_black : this.ablack,
1308 attrib_clean : this.aclean,
1309 style_white : this.cwhite,
1310 style_black : this.cblack
1312 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1313 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1319 * Clean up MS wordisms...
1320 * @deprecated - use filter directly
1322 cleanWord : function(node)
1324 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1331 * @deprecated - use filters
1333 cleanTableWidths : function(node)
1335 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1342 applyBlacklists : function()
1344 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1345 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1347 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1348 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1349 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1353 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1354 if (b.indexOf(tag) > -1) {
1357 this.white.push(tag);
1361 Roo.each(w, function(tag) {
1362 if (b.indexOf(tag) > -1) {
1365 if (this.white.indexOf(tag) > -1) {
1368 this.white.push(tag);
1373 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1374 if (w.indexOf(tag) > -1) {
1377 this.black.push(tag);
1381 Roo.each(b, function(tag) {
1382 if (w.indexOf(tag) > -1) {
1385 if (this.black.indexOf(tag) > -1) {
1388 this.black.push(tag);
1393 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1394 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1398 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1399 if (b.indexOf(tag) > -1) {
1402 this.cwhite.push(tag);
1406 Roo.each(w, function(tag) {
1407 if (b.indexOf(tag) > -1) {
1410 if (this.cwhite.indexOf(tag) > -1) {
1413 this.cwhite.push(tag);
1418 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1419 if (w.indexOf(tag) > -1) {
1422 this.cblack.push(tag);
1426 Roo.each(b, function(tag) {
1427 if (w.indexOf(tag) > -1) {
1430 if (this.cblack.indexOf(tag) > -1) {
1433 this.cblack.push(tag);
1438 setStylesheets : function(stylesheets)
1440 if(typeof(stylesheets) == 'string'){
1441 Roo.get(this.iframe.contentDocument.head).createChild({
1452 Roo.each(stylesheets, function(s) {
1457 Roo.get(_this.iframe.contentDocument.head).createChild({
1469 updateLanguage : function()
1471 if (!this.iframe || !this.iframe.contentDocument) {
1474 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1478 removeStylesheets : function()
1482 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1487 setStyle : function(style)
1489 Roo.get(this.iframe.contentDocument.head).createChild({
1498 // hide stuff that is not compatible
1516 * @cfg {String} fieldClass @hide
1519 * @cfg {String} focusClass @hide
1522 * @cfg {String} autoCreate @hide
1525 * @cfg {String} inputType @hide
1528 * @cfg {String} invalidClass @hide
1531 * @cfg {String} invalidText @hide
1534 * @cfg {String} msgFx @hide
1537 * @cfg {String} validateOnBlur @hide
1541 Roo.HtmlEditorCore.white = [
1542 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1544 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1545 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1546 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1547 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1548 'TABLE', 'UL', 'XMP',
1550 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1553 'DIR', 'MENU', 'OL', 'UL', 'DL',
1559 Roo.HtmlEditorCore.black = [
1560 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1562 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1563 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1564 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1565 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1566 //'FONT' // CLEAN LATER..
1567 'COLGROUP', 'COL' // messy tables.
1571 Roo.HtmlEditorCore.clean = [ // ?? needed???
1572 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1574 Roo.HtmlEditorCore.tag_remove = [
1579 Roo.HtmlEditorCore.ablack = [
1583 Roo.HtmlEditorCore.aclean = [
1584 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1588 Roo.HtmlEditorCore.pwhite= [
1589 'http', 'https', 'mailto'
1592 // white listed style attributes.
1593 Roo.HtmlEditorCore.cwhite= [
1594 // 'text-align', /// default is to allow most things..
1600 // black listed style attributes.
1601 Roo.HtmlEditorCore.cblack= [
1602 // 'font-size' -- this can be set by the project