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 html = div.innerHTML;
384 if (this.autoClean) {
386 new Roo.htmleditor.FilterAttributes({
388 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
389 attrib_clean : ['href', 'src' ]
392 var tidy = new Roo.htmleditor.TidySerializer({
395 html = tidy.serialize(div);
401 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
402 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
404 html = '<div style="'+m[0]+'">' + html + '</div>';
407 html = this.cleanHtml(html);
408 // fix up the special chars.. normaly like back quotes in word...
409 // however we do not want to do this with chinese..
410 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
412 var cc = match.charCodeAt();
414 // Get the character value, handling surrogate pairs
415 if (match.length == 2) {
416 // It's a surrogate pair, calculate the Unicode code point
417 var high = match.charCodeAt(0) - 0xD800;
418 var low = match.charCodeAt(1) - 0xDC00;
419 cc = (high * 0x400) + low + 0x10000;
421 (cc >= 0x4E00 && cc < 0xA000 ) ||
422 (cc >= 0x3400 && cc < 0x4E00 ) ||
423 (cc >= 0xf900 && cc < 0xfb00 )
428 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
429 return "&#" + cc + ";";
436 if(this.owner.fireEvent('beforesync', this, html) !== false){
437 this.el.dom.value = html;
438 this.owner.fireEvent('sync', this, html);
444 * TEXTAREA -> EDITABLE
445 * Protected method that will not generally be called directly. Pushes the value of the textarea
446 * into the iframe editor.
448 pushValue : function()
450 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
451 if(this.initialized){
452 var v = this.el.dom.value.trim();
455 if(this.owner.fireEvent('beforepush', this, v) !== false){
456 var d = (this.doc.body || this.doc.documentElement);
459 this.el.dom.value = d.innerHTML;
460 this.owner.fireEvent('push', this, v);
462 if (this.autoClean) {
463 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
464 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
466 if (this.enableBlocks) {
467 Roo.htmleditor.Block.initAll(this.doc.body);
470 this.updateLanguage();
472 var lc = this.doc.body.lastChild;
473 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
474 // add an extra line at the end.
475 this.doc.body.appendChild(this.doc.createElement('br'));
483 deferFocus : function(){
484 this.focus.defer(10, this);
489 if(this.win && !this.sourceEditMode){
496 assignDocWin: function()
498 var iframe = this.iframe;
501 this.doc = iframe.contentWindow.document;
502 this.win = iframe.contentWindow;
504 // if (!Roo.get(this.frameId)) {
507 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
508 // this.win = Roo.get(this.frameId).dom.contentWindow;
510 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
514 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
515 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
520 initEditor : function(){
521 //console.log("INIT EDITOR");
526 this.doc.designMode="on";
528 this.doc.write(this.getDocMarkup());
531 var dbody = (this.doc.body || this.doc.documentElement);
532 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
533 // this copies styles from the containing element into thsi one..
534 // not sure why we need all of this..
535 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
537 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
538 //ss['background-attachment'] = 'fixed'; // w3c
539 dbody.bgProperties = 'fixed'; // ie
540 dbody.setAttribute("translate", "no");
542 //Roo.DomHelper.applyStyles(dbody, ss);
543 Roo.EventManager.on(this.doc, {
545 'mouseup': this.onEditorEvent,
546 'dblclick': this.onEditorEvent,
547 'click': this.onEditorEvent,
548 'keyup': this.onEditorEvent,
553 Roo.EventManager.on(this.doc, {
554 'paste': this.onPasteEvent,
558 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
561 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
562 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
564 this.initialized = true;
567 // initialize special key events - enter
568 new Roo.htmleditor.KeyEnter({core : this});
572 this.owner.fireEvent('initialize', this);
575 // this is to prevent a href clicks resulting in a redirect?
577 onPasteEvent : function(e,v)
579 // I think we better assume paste is going to be a dirty load of rubish from word..
581 // even pasting into a 'email version' of this widget will have to clean up that mess.
582 var cd = (e.browserEvent.clipboardData || window.clipboardData);
584 // check what type of paste - if it's an image, then handle it differently.
585 if (cd.files && cd.files.length > 0) {
587 var urlAPI = (window.createObjectURL && window) ||
588 (window.URL && URL.revokeObjectURL && URL) ||
589 (window.webkitURL && webkitURL);
591 var url = urlAPI.createObjectURL( cd.files[0]);
592 this.insertAtCursor('<img src=" + url + ">');
595 if (cd.types.indexOf('text/html') < 0 ) {
599 var html = cd.getData('text/html'); // clipboard event
600 if (cd.types.indexOf('text/rtf') > -1) {
601 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
602 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
607 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
608 .map(function(g) { return g.toDataURL(); })
609 .filter(function(g) { return g != 'about:blank'; });
612 html = this.cleanWordChars(html);
614 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
617 var sn = this.getParentElement();
618 // check if d contains a table, and prevent nesting??
619 //Roo.log(d.getElementsByTagName('table'));
621 //Roo.log(sn.closest('table'));
622 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
624 this.insertAtCursor("You can not nest tables");
625 //Roo.log("prevent?"); // fixme -
631 if (images.length > 0) {
632 // replace all v:imagedata - with img.
633 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
634 Roo.each(ar, function(node) {
635 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
636 node.parentNode.removeChild(node);
640 Roo.each(d.getElementsByTagName('img'), function(img, i) {
641 img.setAttribute('src', images[i]);
644 if (this.autoClean) {
645 new Roo.htmleditor.FilterWord({ node : d });
647 new Roo.htmleditor.FilterStyleToTag({ node : d });
648 new Roo.htmleditor.FilterAttributes({
650 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
651 attrib_clean : ['href', 'src' ]
653 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
655 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
656 new Roo.htmleditor.FilterParagraph({ node : d });
657 new Roo.htmleditor.FilterSpan({ node : d });
658 new Roo.htmleditor.FilterLongBr({ node : d });
659 new Roo.htmleditor.FilterComment({ node : d });
663 if (this.enableBlocks) {
665 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
666 if (img.closest('figure')) { // assume!! that it's aready
669 var fig = new Roo.htmleditor.BlockFigure({
672 fig.updateElement(img); // replace it..
678 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
679 if (this.enableBlocks) {
680 Roo.htmleditor.Block.initAll(this.doc.body);
686 // default behaveiour should be our local cleanup paste? (optional?)
687 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
688 //this.owner.fireEvent('paste', e, v);
691 onDestroy : function(){
697 //for (var i =0; i < this.toolbars.length;i++) {
698 // // fixme - ask toolbars for heights?
699 // this.toolbars[i].onDestroy();
702 //this.wrap.dom.innerHTML = '';
703 //this.wrap.remove();
708 onFirstFocus : function(){
711 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
713 this.activated = true;
716 if(Roo.isGecko){ // prevent silly gecko errors
718 var s = this.win.getSelection();
719 if(!s.focusNode || s.focusNode.nodeType != 3){
720 var r = s.getRangeAt(0);
721 r.selectNodeContents((this.doc.body || this.doc.documentElement));
726 this.execCmd('useCSS', true);
727 this.execCmd('styleWithCSS', false);
730 this.owner.fireEvent('activate', this);
734 adjustFont: function(btn){
735 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
736 //if(Roo.isSafari){ // safari
739 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
740 if(Roo.isSafari){ // safari
741 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
742 v = (v < 10) ? 10 : v;
743 v = (v > 48) ? 48 : v;
744 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
749 v = Math.max(1, v+adjust);
751 this.execCmd('FontSize', v );
754 onEditorEvent : function(e)
758 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
759 return; // we do not handle this.. (undo manager does..)
761 // in theory this detects if the last element is not a br, then we try and do that.
762 // its so clicking in space at bottom triggers adding a br and moving the cursor.
764 e.target.nodeName == 'BODY' &&
765 e.type == "mouseup" &&
766 this.doc.body.lastChild
768 var lc = this.doc.body.lastChild;
769 // gtx-trans is google translate plugin adding crap.
770 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
771 lc = lc.previousSibling;
773 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
774 // if last element is <BR> - then dont do anything.
776 var ns = this.doc.createElement('br');
777 this.doc.body.appendChild(ns);
778 range = this.doc.createRange();
779 range.setStartAfter(ns);
780 range.collapse(true);
781 var sel = this.win.getSelection();
782 sel.removeAllRanges();
789 this.fireEditorEvent(e);
790 // this.updateToolbar();
791 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
794 fireEditorEvent: function(e)
796 this.owner.fireEvent('editorevent', this, e);
799 insertTag : function(tg)
801 // could be a bit smarter... -> wrap the current selected tRoo..
802 if (tg.toLowerCase() == 'span' ||
803 tg.toLowerCase() == 'code' ||
804 tg.toLowerCase() == 'sup' ||
805 tg.toLowerCase() == 'sub'
808 range = this.createRange(this.getSelection());
809 var wrappingNode = this.doc.createElement(tg.toLowerCase());
810 wrappingNode.appendChild(range.extractContents());
811 range.insertNode(wrappingNode);
818 this.execCmd("formatblock", tg);
819 this.undoManager.addEvent();
822 insertText : function(txt)
826 var range = this.createRange();
827 range.deleteContents();
828 //alert(Sender.getAttribute('label'));
830 range.insertNode(this.doc.createTextNode(txt));
831 this.undoManager.addEvent();
837 * Executes a Midas editor command on the editor document and performs necessary focus and
838 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
839 * @param {String} cmd The Midas command
840 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
842 relayCmd : function(cmd, value)
848 case 'justifycenter':
849 // if we are in a cell, then we will adjust the
850 var n = this.getParentElement();
851 var td = n.closest('td');
853 var bl = Roo.htmleditor.Block.factory(td);
854 bl.textAlign = cmd.replace('justify','');
856 this.owner.fireEvent('editorevent', this);
859 this.execCmd('styleWithCSS', true); //
863 // if there is no selection, then we insert, and set the curson inside it..
864 this.execCmd('styleWithCSS', false);
874 this.execCmd(cmd, value);
875 this.owner.fireEvent('editorevent', this);
876 //this.updateToolbar();
877 this.owner.deferFocus();
881 * Executes a Midas editor command directly on the editor document.
882 * For visual commands, you should use {@link #relayCmd} instead.
883 * <b>This should only be called after the editor is initialized.</b>
884 * @param {String} cmd The Midas command
885 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
887 execCmd : function(cmd, value){
888 this.doc.execCommand(cmd, false, value === undefined ? null : value);
895 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
897 * @param {String} text | dom node..
899 insertAtCursor : function(text)
906 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
910 // from jquery ui (MIT licenced)
914 if (win.getSelection && win.getSelection().getRangeAt) {
916 // delete the existing?
918 this.createRange(this.getSelection()).deleteContents();
919 range = win.getSelection().getRangeAt(0);
920 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
921 range.insertNode(node);
922 range = range.cloneRange();
923 range.collapse(false);
925 win.getSelection().removeAllRanges();
926 win.getSelection().addRange(range);
930 } else if (win.document.selection && win.document.selection.createRange) {
931 // no firefox support
932 var txt = typeof(text) == 'string' ? text : text.outerHTML;
933 win.document.selection.createRange().pasteHTML(txt);
936 // no firefox support
937 var txt = typeof(text) == 'string' ? text : text.outerHTML;
938 this.execCmd('InsertHTML', txt);
946 mozKeyPress : function(e){
948 var c = e.getCharCode(), cmd;
951 c = String.fromCharCode(c).toLowerCase();
965 // this.cleanUpPaste.defer(100, this);
983 fixKeys : function(){ // load time branching for fastest keydown performance
988 var k = e.getKey(), r;
991 r = this.doc.selection.createRange();
994 r.pasteHTML('    ');
999 /// this is handled by Roo.htmleditor.KeyEnter
1002 r = this.doc.selection.createRange();
1004 var target = r.parentElement();
1005 if(!target || target.tagName.toLowerCase() != 'li'){
1007 r.pasteHTML('<br/>');
1014 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1015 // this.cleanUpPaste.defer(100, this);
1021 }else if(Roo.isOpera){
1027 this.execCmd('InsertHTML','    ');
1031 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1032 // this.cleanUpPaste.defer(100, this);
1037 }else if(Roo.isSafari){
1043 this.execCmd('InsertText','\t');
1047 this.mozKeyPress(e);
1049 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1050 // this.cleanUpPaste.defer(100, this);
1058 getAllAncestors: function()
1060 var p = this.getSelectedNode();
1063 a.push(p); // push blank onto stack..
1064 p = this.getParentElement();
1068 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1072 a.push(this.doc.body);
1076 lastSelNode : false,
1079 getSelection : function()
1081 this.assignDocWin();
1082 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1086 * @param {DomElement} node the node to select
1088 selectNode : function(node, collapse)
1090 var nodeRange = node.ownerDocument.createRange();
1092 nodeRange.selectNode(node);
1094 nodeRange.selectNodeContents(node);
1096 if (collapse === true) {
1097 nodeRange.collapse(true);
1100 var s = this.win.getSelection();
1101 s.removeAllRanges();
1102 s.addRange(nodeRange);
1105 getSelectedNode: function()
1107 // this may only work on Gecko!!!
1109 // should we cache this!!!!
1113 var range = this.createRange(this.getSelection()).cloneRange();
1116 var parent = range.parentElement();
1118 var testRange = range.duplicate();
1119 testRange.moveToElementText(parent);
1120 if (testRange.inRange(range)) {
1123 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1126 parent = parent.parentElement;
1131 // is ancestor a text element.
1132 var ac = range.commonAncestorContainer;
1133 if (ac.nodeType == 3) {
1137 var ar = ac.childNodes;
1140 var other_nodes = [];
1141 var has_other_nodes = false;
1142 for (var i=0;i<ar.length;i++) {
1143 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1146 // fullly contained node.
1148 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1153 // probably selected..
1154 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1155 other_nodes.push(ar[i]);
1159 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1164 has_other_nodes = true;
1166 if (!nodes.length && other_nodes.length) {
1169 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1177 createRange: function(sel)
1179 // this has strange effects when using with
1180 // top toolbar - not sure if it's a great idea.
1181 //this.editor.contentWindow.focus();
1182 if (typeof sel != "undefined") {
1184 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1186 return this.doc.createRange();
1189 return this.doc.createRange();
1192 getParentElement: function()
1195 this.assignDocWin();
1196 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1198 var range = this.createRange(sel);
1201 var p = range.commonAncestorContainer;
1202 while (p.nodeType == 3) { // text node
1213 * Range intersection.. the hard stuff...
1217 * [ -- selected range --- ]
1221 * if end is before start or hits it. fail.
1222 * if start is after end or hits it fail.
1224 * if either hits (but other is outside. - then it's not
1230 // @see http://www.thismuchiknow.co.uk/?p=64.
1231 rangeIntersectsNode : function(range, node)
1233 var nodeRange = node.ownerDocument.createRange();
1235 nodeRange.selectNode(node);
1237 nodeRange.selectNodeContents(node);
1240 var rangeStartRange = range.cloneRange();
1241 rangeStartRange.collapse(true);
1243 var rangeEndRange = range.cloneRange();
1244 rangeEndRange.collapse(false);
1246 var nodeStartRange = nodeRange.cloneRange();
1247 nodeStartRange.collapse(true);
1249 var nodeEndRange = nodeRange.cloneRange();
1250 nodeEndRange.collapse(false);
1252 return rangeStartRange.compareBoundaryPoints(
1253 Range.START_TO_START, nodeEndRange) == -1 &&
1254 rangeEndRange.compareBoundaryPoints(
1255 Range.START_TO_START, nodeStartRange) == 1;
1259 rangeCompareNode : function(range, node)
1261 var nodeRange = node.ownerDocument.createRange();
1263 nodeRange.selectNode(node);
1265 nodeRange.selectNodeContents(node);
1269 range.collapse(true);
1271 nodeRange.collapse(true);
1273 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1274 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1276 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1278 var nodeIsBefore = ss == 1;
1279 var nodeIsAfter = ee == -1;
1281 if (nodeIsBefore && nodeIsAfter) {
1284 if (!nodeIsBefore && nodeIsAfter) {
1285 return 1; //right trailed.
1288 if (nodeIsBefore && !nodeIsAfter) {
1289 return 2; // left trailed.
1295 cleanWordChars : function(input) {// change the chars to hex code
1298 [ 8211, "–" ],
1299 [ 8212, "—" ],
1308 Roo.each(swapCodes, function(sw) {
1309 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1311 output = output.replace(swapper, sw[1]);
1321 cleanUpChild : function (node)
1324 new Roo.htmleditor.FilterComment({node : node});
1325 new Roo.htmleditor.FilterAttributes({
1327 attrib_black : this.ablack,
1328 attrib_clean : this.aclean,
1329 style_white : this.cwhite,
1330 style_black : this.cblack
1332 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1333 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1339 * Clean up MS wordisms...
1340 * @deprecated - use filter directly
1342 cleanWord : function(node)
1344 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1345 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1352 * @deprecated - use filters
1354 cleanTableWidths : function(node)
1356 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1363 applyBlacklists : function()
1365 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1366 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1368 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1369 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1370 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1374 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1375 if (b.indexOf(tag) > -1) {
1378 this.white.push(tag);
1382 Roo.each(w, function(tag) {
1383 if (b.indexOf(tag) > -1) {
1386 if (this.white.indexOf(tag) > -1) {
1389 this.white.push(tag);
1394 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1395 if (w.indexOf(tag) > -1) {
1398 this.black.push(tag);
1402 Roo.each(b, function(tag) {
1403 if (w.indexOf(tag) > -1) {
1406 if (this.black.indexOf(tag) > -1) {
1409 this.black.push(tag);
1414 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1415 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1419 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1420 if (b.indexOf(tag) > -1) {
1423 this.cwhite.push(tag);
1427 Roo.each(w, function(tag) {
1428 if (b.indexOf(tag) > -1) {
1431 if (this.cwhite.indexOf(tag) > -1) {
1434 this.cwhite.push(tag);
1439 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1440 if (w.indexOf(tag) > -1) {
1443 this.cblack.push(tag);
1447 Roo.each(b, function(tag) {
1448 if (w.indexOf(tag) > -1) {
1451 if (this.cblack.indexOf(tag) > -1) {
1454 this.cblack.push(tag);
1459 setStylesheets : function(stylesheets)
1461 if(typeof(stylesheets) == 'string'){
1462 Roo.get(this.iframe.contentDocument.head).createChild({
1473 Roo.each(stylesheets, function(s) {
1478 Roo.get(_this.iframe.contentDocument.head).createChild({
1490 updateLanguage : function()
1492 if (!this.iframe || !this.iframe.contentDocument) {
1495 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1499 removeStylesheets : function()
1503 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1508 setStyle : function(style)
1510 Roo.get(this.iframe.contentDocument.head).createChild({
1519 // hide stuff that is not compatible
1537 * @cfg {String} fieldClass @hide
1540 * @cfg {String} focusClass @hide
1543 * @cfg {String} autoCreate @hide
1546 * @cfg {String} inputType @hide
1549 * @cfg {String} invalidClass @hide
1552 * @cfg {String} invalidText @hide
1555 * @cfg {String} msgFx @hide
1558 * @cfg {String} validateOnBlur @hide
1562 Roo.HtmlEditorCore.white = [
1563 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1565 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1566 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1567 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1568 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1569 'TABLE', 'UL', 'XMP',
1571 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1574 'DIR', 'MENU', 'OL', 'UL', 'DL',
1580 Roo.HtmlEditorCore.black = [
1581 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1583 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1584 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1585 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1586 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1587 //'FONT' // CLEAN LATER..
1588 'COLGROUP', 'COL' // messy tables.
1592 Roo.HtmlEditorCore.clean = [ // ?? needed???
1593 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1595 Roo.HtmlEditorCore.tag_remove = [
1600 Roo.HtmlEditorCore.ablack = [
1604 Roo.HtmlEditorCore.aclean = [
1605 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1609 Roo.HtmlEditorCore.pwhite= [
1610 'http', 'https', 'mailto'
1613 // white listed style attributes.
1614 Roo.HtmlEditorCore.cwhite= [
1615 // 'text-align', /// default is to allow most things..
1621 // black listed style attributes.
1622 Roo.HtmlEditorCore.cblack= [
1623 // 'font-size' -- this can be set by the project