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 -
618 if (images.length > 0) {
619 // replace all v:imagedata - with img.
620 Roo.each(d.getElementsByTagName('v:imagedata'), function(node) {
621 node.parentNode.insertBefore(node, d.createElement('img'));
622 node.parentNode.removeChild(node);
626 Roo.each(d.getElementsByTagName('img'), function(img, i) {
627 img.setAttribute('src', images[i]);
630 if (this.autoClean) {
631 new Roo.htmleditor.FilterWord({ node : d });
633 new Roo.htmleditor.FilterStyleToTag({ node : d });
634 new Roo.htmleditor.FilterAttributes({
636 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width'],
637 attrib_clean : ['href', 'src' ]
639 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
641 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
642 new Roo.htmleditor.FilterParagraph({ node : d });
643 new Roo.htmleditor.FilterSpan({ node : d });
644 new Roo.htmleditor.FilterLongBr({ node : d });
645 new Roo.htmleditor.FilterComment({ node : d });
649 if (this.enableBlocks) {
651 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
652 if (img.closest('figure')) { // assume!! that it's aready
655 var fig = new Roo.htmleditor.BlockFigure({
658 fig.updateElement(img); // replace it..
664 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
665 if (this.enableBlocks) {
666 Roo.htmleditor.Block.initAll(this.doc.body);
672 // default behaveiour should be our local cleanup paste? (optional?)
673 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
674 //this.owner.fireEvent('paste', e, v);
677 onDestroy : function(){
683 //for (var i =0; i < this.toolbars.length;i++) {
684 // // fixme - ask toolbars for heights?
685 // this.toolbars[i].onDestroy();
688 //this.wrap.dom.innerHTML = '';
689 //this.wrap.remove();
694 onFirstFocus : function(){
697 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
699 this.activated = true;
702 if(Roo.isGecko){ // prevent silly gecko errors
704 var s = this.win.getSelection();
705 if(!s.focusNode || s.focusNode.nodeType != 3){
706 var r = s.getRangeAt(0);
707 r.selectNodeContents((this.doc.body || this.doc.documentElement));
712 this.execCmd('useCSS', true);
713 this.execCmd('styleWithCSS', false);
716 this.owner.fireEvent('activate', this);
720 adjustFont: function(btn){
721 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
722 //if(Roo.isSafari){ // safari
725 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
726 if(Roo.isSafari){ // safari
727 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
728 v = (v < 10) ? 10 : v;
729 v = (v > 48) ? 48 : v;
730 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
735 v = Math.max(1, v+adjust);
737 this.execCmd('FontSize', v );
740 onEditorEvent : function(e)
744 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
745 return; // we do not handle this.. (undo manager does..)
747 // in theory this detects if the last element is not a br, then we try and do that.
748 // its so clicking in space at bottom triggers adding a br and moving the cursor.
750 e.target.nodeName == 'BODY' &&
751 e.type == "mouseup" &&
752 this.doc.body.lastChild
754 var lc = this.doc.body.lastChild;
755 // gtx-trans is google translate plugin adding crap.
756 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
757 lc = lc.previousSibling;
759 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
760 // if last element is <BR> - then dont do anything.
762 var ns = this.doc.createElement('br');
763 this.doc.body.appendChild(ns);
764 range = this.doc.createRange();
765 range.setStartAfter(ns);
766 range.collapse(true);
767 var sel = this.win.getSelection();
768 sel.removeAllRanges();
775 this.fireEditorEvent(e);
776 // this.updateToolbar();
777 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
780 fireEditorEvent: function(e)
782 this.owner.fireEvent('editorevent', this, e);
785 insertTag : function(tg)
787 // could be a bit smarter... -> wrap the current selected tRoo..
788 if (tg.toLowerCase() == 'span' ||
789 tg.toLowerCase() == 'code' ||
790 tg.toLowerCase() == 'sup' ||
791 tg.toLowerCase() == 'sub'
794 range = this.createRange(this.getSelection());
795 var wrappingNode = this.doc.createElement(tg.toLowerCase());
796 wrappingNode.appendChild(range.extractContents());
797 range.insertNode(wrappingNode);
804 this.execCmd("formatblock", tg);
805 this.undoManager.addEvent();
808 insertText : function(txt)
812 var range = this.createRange();
813 range.deleteContents();
814 //alert(Sender.getAttribute('label'));
816 range.insertNode(this.doc.createTextNode(txt));
817 this.undoManager.addEvent();
823 * Executes a Midas editor command on the editor document and performs necessary focus and
824 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
825 * @param {String} cmd The Midas command
826 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
828 relayCmd : function(cmd, value)
834 case 'justifycenter':
835 // if we are in a cell, then we will adjust the
836 var n = this.getParentElement();
837 var td = n.closest('td');
839 var bl = Roo.htmleditor.Block.factory(td);
840 bl.textAlign = cmd.replace('justify','');
842 this.owner.fireEvent('editorevent', this);
845 this.execCmd('styleWithCSS', true); //
849 // if there is no selection, then we insert, and set the curson inside it..
850 this.execCmd('styleWithCSS', false);
860 this.execCmd(cmd, value);
861 this.owner.fireEvent('editorevent', this);
862 //this.updateToolbar();
863 this.owner.deferFocus();
867 * Executes a Midas editor command directly on the editor document.
868 * For visual commands, you should use {@link #relayCmd} instead.
869 * <b>This should only be called after the editor is initialized.</b>
870 * @param {String} cmd The Midas command
871 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
873 execCmd : function(cmd, value){
874 this.doc.execCommand(cmd, false, value === undefined ? null : value);
881 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
883 * @param {String} text | dom node..
885 insertAtCursor : function(text)
892 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
896 // from jquery ui (MIT licenced)
900 if (win.getSelection && win.getSelection().getRangeAt) {
902 // delete the existing?
904 this.createRange(this.getSelection()).deleteContents();
905 range = win.getSelection().getRangeAt(0);
906 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
907 range.insertNode(node);
908 range = range.cloneRange();
909 range.collapse(false);
911 win.getSelection().removeAllRanges();
912 win.getSelection().addRange(range);
916 } else if (win.document.selection && win.document.selection.createRange) {
917 // no firefox support
918 var txt = typeof(text) == 'string' ? text : text.outerHTML;
919 win.document.selection.createRange().pasteHTML(txt);
922 // no firefox support
923 var txt = typeof(text) == 'string' ? text : text.outerHTML;
924 this.execCmd('InsertHTML', txt);
932 mozKeyPress : function(e){
934 var c = e.getCharCode(), cmd;
937 c = String.fromCharCode(c).toLowerCase();
951 // this.cleanUpPaste.defer(100, this);
969 fixKeys : function(){ // load time branching for fastest keydown performance
974 var k = e.getKey(), r;
977 r = this.doc.selection.createRange();
980 r.pasteHTML('    ');
985 /// this is handled by Roo.htmleditor.KeyEnter
988 r = this.doc.selection.createRange();
990 var target = r.parentElement();
991 if(!target || target.tagName.toLowerCase() != 'li'){
993 r.pasteHTML('<br/>');
1000 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1001 // this.cleanUpPaste.defer(100, this);
1007 }else if(Roo.isOpera){
1013 this.execCmd('InsertHTML','    ');
1017 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1018 // this.cleanUpPaste.defer(100, this);
1023 }else if(Roo.isSafari){
1029 this.execCmd('InsertText','\t');
1033 this.mozKeyPress(e);
1035 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1036 // this.cleanUpPaste.defer(100, this);
1044 getAllAncestors: function()
1046 var p = this.getSelectedNode();
1049 a.push(p); // push blank onto stack..
1050 p = this.getParentElement();
1054 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1058 a.push(this.doc.body);
1062 lastSelNode : false,
1065 getSelection : function()
1067 this.assignDocWin();
1068 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1072 * @param {DomElement} node the node to select
1074 selectNode : function(node, collapse)
1076 var nodeRange = node.ownerDocument.createRange();
1078 nodeRange.selectNode(node);
1080 nodeRange.selectNodeContents(node);
1082 if (collapse === true) {
1083 nodeRange.collapse(true);
1086 var s = this.win.getSelection();
1087 s.removeAllRanges();
1088 s.addRange(nodeRange);
1091 getSelectedNode: function()
1093 // this may only work on Gecko!!!
1095 // should we cache this!!!!
1099 var range = this.createRange(this.getSelection()).cloneRange();
1102 var parent = range.parentElement();
1104 var testRange = range.duplicate();
1105 testRange.moveToElementText(parent);
1106 if (testRange.inRange(range)) {
1109 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1112 parent = parent.parentElement;
1117 // is ancestor a text element.
1118 var ac = range.commonAncestorContainer;
1119 if (ac.nodeType == 3) {
1123 var ar = ac.childNodes;
1126 var other_nodes = [];
1127 var has_other_nodes = false;
1128 for (var i=0;i<ar.length;i++) {
1129 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1132 // fullly contained node.
1134 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1139 // probably selected..
1140 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1141 other_nodes.push(ar[i]);
1145 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1150 has_other_nodes = true;
1152 if (!nodes.length && other_nodes.length) {
1155 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1163 createRange: function(sel)
1165 // this has strange effects when using with
1166 // top toolbar - not sure if it's a great idea.
1167 //this.editor.contentWindow.focus();
1168 if (typeof sel != "undefined") {
1170 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1172 return this.doc.createRange();
1175 return this.doc.createRange();
1178 getParentElement: function()
1181 this.assignDocWin();
1182 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1184 var range = this.createRange(sel);
1187 var p = range.commonAncestorContainer;
1188 while (p.nodeType == 3) { // text node
1199 * Range intersection.. the hard stuff...
1203 * [ -- selected range --- ]
1207 * if end is before start or hits it. fail.
1208 * if start is after end or hits it fail.
1210 * if either hits (but other is outside. - then it's not
1216 // @see http://www.thismuchiknow.co.uk/?p=64.
1217 rangeIntersectsNode : function(range, node)
1219 var nodeRange = node.ownerDocument.createRange();
1221 nodeRange.selectNode(node);
1223 nodeRange.selectNodeContents(node);
1226 var rangeStartRange = range.cloneRange();
1227 rangeStartRange.collapse(true);
1229 var rangeEndRange = range.cloneRange();
1230 rangeEndRange.collapse(false);
1232 var nodeStartRange = nodeRange.cloneRange();
1233 nodeStartRange.collapse(true);
1235 var nodeEndRange = nodeRange.cloneRange();
1236 nodeEndRange.collapse(false);
1238 return rangeStartRange.compareBoundaryPoints(
1239 Range.START_TO_START, nodeEndRange) == -1 &&
1240 rangeEndRange.compareBoundaryPoints(
1241 Range.START_TO_START, nodeStartRange) == 1;
1245 rangeCompareNode : function(range, node)
1247 var nodeRange = node.ownerDocument.createRange();
1249 nodeRange.selectNode(node);
1251 nodeRange.selectNodeContents(node);
1255 range.collapse(true);
1257 nodeRange.collapse(true);
1259 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1260 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1262 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1264 var nodeIsBefore = ss == 1;
1265 var nodeIsAfter = ee == -1;
1267 if (nodeIsBefore && nodeIsAfter) {
1270 if (!nodeIsBefore && nodeIsAfter) {
1271 return 1; //right trailed.
1274 if (nodeIsBefore && !nodeIsAfter) {
1275 return 2; // left trailed.
1281 cleanWordChars : function(input) {// change the chars to hex code
1284 [ 8211, "–" ],
1285 [ 8212, "—" ],
1294 Roo.each(swapCodes, function(sw) {
1295 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1297 output = output.replace(swapper, sw[1]);
1307 cleanUpChild : function (node)
1310 new Roo.htmleditor.FilterComment({node : node});
1311 new Roo.htmleditor.FilterAttributes({
1313 attrib_black : this.ablack,
1314 attrib_clean : this.aclean,
1315 style_white : this.cwhite,
1316 style_black : this.cblack
1318 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1319 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1325 * Clean up MS wordisms...
1326 * @deprecated - use filter directly
1328 cleanWord : function(node)
1330 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1331 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1338 * @deprecated - use filters
1340 cleanTableWidths : function(node)
1342 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1349 applyBlacklists : function()
1351 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1352 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1354 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1355 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1356 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1360 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1361 if (b.indexOf(tag) > -1) {
1364 this.white.push(tag);
1368 Roo.each(w, function(tag) {
1369 if (b.indexOf(tag) > -1) {
1372 if (this.white.indexOf(tag) > -1) {
1375 this.white.push(tag);
1380 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1381 if (w.indexOf(tag) > -1) {
1384 this.black.push(tag);
1388 Roo.each(b, function(tag) {
1389 if (w.indexOf(tag) > -1) {
1392 if (this.black.indexOf(tag) > -1) {
1395 this.black.push(tag);
1400 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1401 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1405 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1406 if (b.indexOf(tag) > -1) {
1409 this.cwhite.push(tag);
1413 Roo.each(w, function(tag) {
1414 if (b.indexOf(tag) > -1) {
1417 if (this.cwhite.indexOf(tag) > -1) {
1420 this.cwhite.push(tag);
1425 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1426 if (w.indexOf(tag) > -1) {
1429 this.cblack.push(tag);
1433 Roo.each(b, function(tag) {
1434 if (w.indexOf(tag) > -1) {
1437 if (this.cblack.indexOf(tag) > -1) {
1440 this.cblack.push(tag);
1445 setStylesheets : function(stylesheets)
1447 if(typeof(stylesheets) == 'string'){
1448 Roo.get(this.iframe.contentDocument.head).createChild({
1459 Roo.each(stylesheets, function(s) {
1464 Roo.get(_this.iframe.contentDocument.head).createChild({
1476 updateLanguage : function()
1478 if (!this.iframe || !this.iframe.contentDocument) {
1481 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1485 removeStylesheets : function()
1489 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1494 setStyle : function(style)
1496 Roo.get(this.iframe.contentDocument.head).createChild({
1505 // hide stuff that is not compatible
1523 * @cfg {String} fieldClass @hide
1526 * @cfg {String} focusClass @hide
1529 * @cfg {String} autoCreate @hide
1532 * @cfg {String} inputType @hide
1535 * @cfg {String} invalidClass @hide
1538 * @cfg {String} invalidText @hide
1541 * @cfg {String} msgFx @hide
1544 * @cfg {String} validateOnBlur @hide
1548 Roo.HtmlEditorCore.white = [
1549 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1551 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1552 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1553 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1554 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1555 'TABLE', 'UL', 'XMP',
1557 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1560 'DIR', 'MENU', 'OL', 'UL', 'DL',
1566 Roo.HtmlEditorCore.black = [
1567 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1569 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1570 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1571 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1572 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1573 //'FONT' // CLEAN LATER..
1574 'COLGROUP', 'COL' // messy tables.
1578 Roo.HtmlEditorCore.clean = [ // ?? needed???
1579 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1581 Roo.HtmlEditorCore.tag_remove = [
1586 Roo.HtmlEditorCore.ablack = [
1590 Roo.HtmlEditorCore.aclean = [
1591 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1595 Roo.HtmlEditorCore.pwhite= [
1596 'http', 'https', 'mailto'
1599 // white listed style attributes.
1600 Roo.HtmlEditorCore.cwhite= [
1601 // 'text-align', /// default is to allow most things..
1607 // black listed style attributes.
1608 Roo.HtmlEditorCore.cblack= [
1609 // 'font-size' -- this can be set by the project