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'],
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 });
636 if (this.enableBlocks) {
638 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
639 if (img.closest('figure')) { // assume!! that it's aready
642 var fig = new Roo.htmleditor.BlockFigure({
645 fig.updateElement(img); // replace it..
651 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
652 if (this.enableBlocks) {
653 Roo.htmleditor.Block.initAll(this.doc.body);
659 // default behaveiour should be our local cleanup paste? (optional?)
660 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
661 //this.owner.fireEvent('paste', e, v);
664 onDestroy : function(){
670 //for (var i =0; i < this.toolbars.length;i++) {
671 // // fixme - ask toolbars for heights?
672 // this.toolbars[i].onDestroy();
675 //this.wrap.dom.innerHTML = '';
676 //this.wrap.remove();
681 onFirstFocus : function(){
684 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
686 this.activated = true;
689 if(Roo.isGecko){ // prevent silly gecko errors
691 var s = this.win.getSelection();
692 if(!s.focusNode || s.focusNode.nodeType != 3){
693 var r = s.getRangeAt(0);
694 r.selectNodeContents((this.doc.body || this.doc.documentElement));
699 this.execCmd('useCSS', true);
700 this.execCmd('styleWithCSS', false);
703 this.owner.fireEvent('activate', this);
707 adjustFont: function(btn){
708 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
709 //if(Roo.isSafari){ // safari
712 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
713 if(Roo.isSafari){ // safari
714 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
715 v = (v < 10) ? 10 : v;
716 v = (v > 48) ? 48 : v;
717 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
722 v = Math.max(1, v+adjust);
724 this.execCmd('FontSize', v );
727 onEditorEvent : function(e)
731 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
732 return; // we do not handle this.. (undo manager does..)
734 // in theory this detects if the last element is not a br, then we try and do that.
735 // its so clicking in space at bottom triggers adding a br and moving the cursor.
737 e.target.nodeName == 'BODY' &&
738 e.type == "mouseup" &&
739 this.doc.body.lastChild
741 var lc = this.doc.body.lastChild;
742 // gtx-trans is google translate plugin adding crap.
743 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
744 lc = lc.previousSibling;
746 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
747 // if last element is <BR> - then dont do anything.
749 var ns = this.doc.createElement('br');
750 this.doc.body.appendChild(ns);
751 range = this.doc.createRange();
752 range.setStartAfter(ns);
753 range.collapse(true);
754 var sel = this.win.getSelection();
755 sel.removeAllRanges();
762 this.fireEditorEvent(e);
763 // this.updateToolbar();
764 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
767 fireEditorEvent: function(e)
769 this.owner.fireEvent('editorevent', this, e);
772 insertTag : function(tg)
774 // could be a bit smarter... -> wrap the current selected tRoo..
775 if (tg.toLowerCase() == 'span' ||
776 tg.toLowerCase() == 'code' ||
777 tg.toLowerCase() == 'sup' ||
778 tg.toLowerCase() == 'sub'
781 range = this.createRange(this.getSelection());
782 var wrappingNode = this.doc.createElement(tg.toLowerCase());
783 wrappingNode.appendChild(range.extractContents());
784 range.insertNode(wrappingNode);
791 this.execCmd("formatblock", tg);
792 this.undoManager.addEvent();
795 insertText : function(txt)
799 var range = this.createRange();
800 range.deleteContents();
801 //alert(Sender.getAttribute('label'));
803 range.insertNode(this.doc.createTextNode(txt));
804 this.undoManager.addEvent();
810 * Executes a Midas editor command on the editor document and performs necessary focus and
811 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
812 * @param {String} cmd The Midas command
813 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
815 relayCmd : function(cmd, value)
821 case 'justifycenter':
822 // if we are in a cell, then we will adjust the
823 var n = this.getParentElement();
824 var td = n.closest('td');
826 var bl = Roo.htmleditor.Block.factory(td);
827 bl.textAlign = cmd.replace('justify','');
829 this.owner.fireEvent('editorevent', this);
832 this.execCmd('styleWithCSS', true); //
836 // if there is no selection, then we insert, and set the curson inside it..
837 this.execCmd('styleWithCSS', false);
847 this.execCmd(cmd, value);
848 this.owner.fireEvent('editorevent', this);
849 //this.updateToolbar();
850 this.owner.deferFocus();
854 * Executes a Midas editor command directly on the editor document.
855 * For visual commands, you should use {@link #relayCmd} instead.
856 * <b>This should only be called after the editor is initialized.</b>
857 * @param {String} cmd The Midas command
858 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
860 execCmd : function(cmd, value){
861 this.doc.execCommand(cmd, false, value === undefined ? null : value);
868 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
870 * @param {String} text | dom node..
872 insertAtCursor : function(text)
879 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
883 // from jquery ui (MIT licenced)
887 if (win.getSelection && win.getSelection().getRangeAt) {
889 // delete the existing?
891 this.createRange(this.getSelection()).deleteContents();
892 range = win.getSelection().getRangeAt(0);
893 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
894 range.insertNode(node);
895 range = range.cloneRange();
896 range.collapse(false);
898 win.getSelection().removeAllRanges();
899 win.getSelection().addRange(range);
903 } else if (win.document.selection && win.document.selection.createRange) {
904 // no firefox support
905 var txt = typeof(text) == 'string' ? text : text.outerHTML;
906 win.document.selection.createRange().pasteHTML(txt);
909 // no firefox support
910 var txt = typeof(text) == 'string' ? text : text.outerHTML;
911 this.execCmd('InsertHTML', txt);
919 mozKeyPress : function(e){
921 var c = e.getCharCode(), cmd;
924 c = String.fromCharCode(c).toLowerCase();
938 // this.cleanUpPaste.defer(100, this);
956 fixKeys : function(){ // load time branching for fastest keydown performance
961 var k = e.getKey(), r;
964 r = this.doc.selection.createRange();
967 r.pasteHTML('    ');
972 /// this is handled by Roo.htmleditor.KeyEnter
975 r = this.doc.selection.createRange();
977 var target = r.parentElement();
978 if(!target || target.tagName.toLowerCase() != 'li'){
980 r.pasteHTML('<br/>');
987 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
988 // this.cleanUpPaste.defer(100, this);
994 }else if(Roo.isOpera){
1000 this.execCmd('InsertHTML','    ');
1004 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1005 // this.cleanUpPaste.defer(100, this);
1010 }else if(Roo.isSafari){
1016 this.execCmd('InsertText','\t');
1020 this.mozKeyPress(e);
1022 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1023 // this.cleanUpPaste.defer(100, this);
1031 getAllAncestors: function()
1033 var p = this.getSelectedNode();
1036 a.push(p); // push blank onto stack..
1037 p = this.getParentElement();
1041 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1045 a.push(this.doc.body);
1049 lastSelNode : false,
1052 getSelection : function()
1054 this.assignDocWin();
1055 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1059 * @param {DomElement} node the node to select
1061 selectNode : function(node, collapse)
1063 var nodeRange = node.ownerDocument.createRange();
1065 nodeRange.selectNode(node);
1067 nodeRange.selectNodeContents(node);
1069 if (collapse === true) {
1070 nodeRange.collapse(true);
1073 var s = this.win.getSelection();
1074 s.removeAllRanges();
1075 s.addRange(nodeRange);
1078 getSelectedNode: function()
1080 // this may only work on Gecko!!!
1082 // should we cache this!!!!
1086 var range = this.createRange(this.getSelection()).cloneRange();
1089 var parent = range.parentElement();
1091 var testRange = range.duplicate();
1092 testRange.moveToElementText(parent);
1093 if (testRange.inRange(range)) {
1096 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1099 parent = parent.parentElement;
1104 // is ancestor a text element.
1105 var ac = range.commonAncestorContainer;
1106 if (ac.nodeType == 3) {
1110 var ar = ac.childNodes;
1113 var other_nodes = [];
1114 var has_other_nodes = false;
1115 for (var i=0;i<ar.length;i++) {
1116 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1119 // fullly contained node.
1121 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1126 // probably selected..
1127 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1128 other_nodes.push(ar[i]);
1132 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1137 has_other_nodes = true;
1139 if (!nodes.length && other_nodes.length) {
1142 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1150 createRange: function(sel)
1152 // this has strange effects when using with
1153 // top toolbar - not sure if it's a great idea.
1154 //this.editor.contentWindow.focus();
1155 if (typeof sel != "undefined") {
1157 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1159 return this.doc.createRange();
1162 return this.doc.createRange();
1165 getParentElement: function()
1168 this.assignDocWin();
1169 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1171 var range = this.createRange(sel);
1174 var p = range.commonAncestorContainer;
1175 while (p.nodeType == 3) { // text node
1186 * Range intersection.. the hard stuff...
1190 * [ -- selected range --- ]
1194 * if end is before start or hits it. fail.
1195 * if start is after end or hits it fail.
1197 * if either hits (but other is outside. - then it's not
1203 // @see http://www.thismuchiknow.co.uk/?p=64.
1204 rangeIntersectsNode : function(range, node)
1206 var nodeRange = node.ownerDocument.createRange();
1208 nodeRange.selectNode(node);
1210 nodeRange.selectNodeContents(node);
1213 var rangeStartRange = range.cloneRange();
1214 rangeStartRange.collapse(true);
1216 var rangeEndRange = range.cloneRange();
1217 rangeEndRange.collapse(false);
1219 var nodeStartRange = nodeRange.cloneRange();
1220 nodeStartRange.collapse(true);
1222 var nodeEndRange = nodeRange.cloneRange();
1223 nodeEndRange.collapse(false);
1225 return rangeStartRange.compareBoundaryPoints(
1226 Range.START_TO_START, nodeEndRange) == -1 &&
1227 rangeEndRange.compareBoundaryPoints(
1228 Range.START_TO_START, nodeStartRange) == 1;
1232 rangeCompareNode : function(range, node)
1234 var nodeRange = node.ownerDocument.createRange();
1236 nodeRange.selectNode(node);
1238 nodeRange.selectNodeContents(node);
1242 range.collapse(true);
1244 nodeRange.collapse(true);
1246 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1247 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1249 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1251 var nodeIsBefore = ss == 1;
1252 var nodeIsAfter = ee == -1;
1254 if (nodeIsBefore && nodeIsAfter) {
1257 if (!nodeIsBefore && nodeIsAfter) {
1258 return 1; //right trailed.
1261 if (nodeIsBefore && !nodeIsAfter) {
1262 return 2; // left trailed.
1268 cleanWordChars : function(input) {// change the chars to hex code
1271 [ 8211, "–" ],
1272 [ 8212, "—" ],
1281 Roo.each(swapCodes, function(sw) {
1282 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1284 output = output.replace(swapper, sw[1]);
1294 cleanUpChild : function (node)
1297 new Roo.htmleditor.FilterComment({node : node});
1298 new Roo.htmleditor.FilterAttributes({
1300 attrib_black : this.ablack,
1301 attrib_clean : this.aclean,
1302 style_white : this.cwhite,
1303 style_black : this.cblack
1305 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1306 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1312 * Clean up MS wordisms...
1313 * @deprecated - use filter directly
1315 cleanWord : function(node)
1317 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1324 * @deprecated - use filters
1326 cleanTableWidths : function(node)
1328 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1335 applyBlacklists : function()
1337 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1338 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1340 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1341 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1342 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1346 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1347 if (b.indexOf(tag) > -1) {
1350 this.white.push(tag);
1354 Roo.each(w, function(tag) {
1355 if (b.indexOf(tag) > -1) {
1358 if (this.white.indexOf(tag) > -1) {
1361 this.white.push(tag);
1366 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1367 if (w.indexOf(tag) > -1) {
1370 this.black.push(tag);
1374 Roo.each(b, function(tag) {
1375 if (w.indexOf(tag) > -1) {
1378 if (this.black.indexOf(tag) > -1) {
1381 this.black.push(tag);
1386 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1387 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1391 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1392 if (b.indexOf(tag) > -1) {
1395 this.cwhite.push(tag);
1399 Roo.each(w, function(tag) {
1400 if (b.indexOf(tag) > -1) {
1403 if (this.cwhite.indexOf(tag) > -1) {
1406 this.cwhite.push(tag);
1411 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1412 if (w.indexOf(tag) > -1) {
1415 this.cblack.push(tag);
1419 Roo.each(b, function(tag) {
1420 if (w.indexOf(tag) > -1) {
1423 if (this.cblack.indexOf(tag) > -1) {
1426 this.cblack.push(tag);
1431 setStylesheets : function(stylesheets)
1433 if(typeof(stylesheets) == 'string'){
1434 Roo.get(this.iframe.contentDocument.head).createChild({
1445 Roo.each(stylesheets, function(s) {
1450 Roo.get(_this.iframe.contentDocument.head).createChild({
1462 updateLanguage : function()
1464 if (!this.iframe || !this.iframe.contentDocument) {
1467 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1471 removeStylesheets : function()
1475 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1480 setStyle : function(style)
1482 Roo.get(this.iframe.contentDocument.head).createChild({
1491 // hide stuff that is not compatible
1509 * @cfg {String} fieldClass @hide
1512 * @cfg {String} focusClass @hide
1515 * @cfg {String} autoCreate @hide
1518 * @cfg {String} inputType @hide
1521 * @cfg {String} invalidClass @hide
1524 * @cfg {String} invalidText @hide
1527 * @cfg {String} msgFx @hide
1530 * @cfg {String} validateOnBlur @hide
1534 Roo.HtmlEditorCore.white = [
1535 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1537 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1538 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1539 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1540 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1541 'TABLE', 'UL', 'XMP',
1543 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1546 'DIR', 'MENU', 'OL', 'UL', 'DL',
1552 Roo.HtmlEditorCore.black = [
1553 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1555 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1556 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1557 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1558 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1559 //'FONT' // CLEAN LATER..
1560 'COLGROUP', 'COL' // messy tables.
1564 Roo.HtmlEditorCore.clean = [ // ?? needed???
1565 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1567 Roo.HtmlEditorCore.tag_remove = [
1572 Roo.HtmlEditorCore.ablack = [
1576 Roo.HtmlEditorCore.aclean = [
1577 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1581 Roo.HtmlEditorCore.pwhite= [
1582 'http', 'https', 'mailto'
1585 // white listed style attributes.
1586 Roo.HtmlEditorCore.cwhite= [
1587 // 'text-align', /// default is to allow most things..
1593 // black listed style attributes.
1594 Roo.HtmlEditorCore.cblack= [
1595 // 'font-size' -- this can be set by the project