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) {
385 var tidy = new Roo.htmleditor.TidySerializer({
388 html = tidy.serialize(div);
394 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
395 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
397 html = '<div style="'+m[0]+'">' + html + '</div>';
400 html = this.cleanHtml(html);
401 // fix up the special chars.. normaly like back quotes in word...
402 // however we do not want to do this with chinese..
403 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
405 var cc = match.charCodeAt();
407 // Get the character value, handling surrogate pairs
408 if (match.length == 2) {
409 // It's a surrogate pair, calculate the Unicode code point
410 var high = match.charCodeAt(0) - 0xD800;
411 var low = match.charCodeAt(1) - 0xDC00;
412 cc = (high * 0x400) + low + 0x10000;
414 (cc >= 0x4E00 && cc < 0xA000 ) ||
415 (cc >= 0x3400 && cc < 0x4E00 ) ||
416 (cc >= 0xf900 && cc < 0xfb00 )
421 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
422 return "&#" + cc + ";";
429 if(this.owner.fireEvent('beforesync', this, html) !== false){
430 this.el.dom.value = html;
431 this.owner.fireEvent('sync', this, html);
437 * TEXTAREA -> EDITABLE
438 * Protected method that will not generally be called directly. Pushes the value of the textarea
439 * into the iframe editor.
441 pushValue : function()
443 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
444 if(this.initialized){
445 var v = this.el.dom.value.trim();
448 if(this.owner.fireEvent('beforepush', this, v) !== false){
449 var d = (this.doc.body || this.doc.documentElement);
452 this.el.dom.value = d.innerHTML;
453 this.owner.fireEvent('push', this, v);
455 if (this.autoClean) {
456 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
457 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
459 if (this.enableBlocks) {
460 Roo.htmleditor.Block.initAll(this.doc.body);
463 this.updateLanguage();
465 var lc = this.doc.body.lastChild;
466 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
467 // add an extra line at the end.
468 this.doc.body.appendChild(this.doc.createElement('br'));
476 deferFocus : function(){
477 this.focus.defer(10, this);
482 if(this.win && !this.sourceEditMode){
489 assignDocWin: function()
491 var iframe = this.iframe;
494 this.doc = iframe.contentWindow.document;
495 this.win = iframe.contentWindow;
497 // if (!Roo.get(this.frameId)) {
500 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
501 // this.win = Roo.get(this.frameId).dom.contentWindow;
503 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
507 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
508 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
513 initEditor : function(){
514 //console.log("INIT EDITOR");
519 this.doc.designMode="on";
521 this.doc.write(this.getDocMarkup());
524 var dbody = (this.doc.body || this.doc.documentElement);
525 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
526 // this copies styles from the containing element into thsi one..
527 // not sure why we need all of this..
528 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
530 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
531 //ss['background-attachment'] = 'fixed'; // w3c
532 dbody.bgProperties = 'fixed'; // ie
533 dbody.setAttribute("translate", "no");
535 //Roo.DomHelper.applyStyles(dbody, ss);
536 Roo.EventManager.on(this.doc, {
538 'mouseup': this.onEditorEvent,
539 'dblclick': this.onEditorEvent,
540 'click': this.onEditorEvent,
541 'keyup': this.onEditorEvent,
546 Roo.EventManager.on(this.doc, {
547 'paste': this.onPasteEvent,
551 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
554 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
555 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
557 this.initialized = true;
560 // initialize special key events - enter
561 new Roo.htmleditor.KeyEnter({core : this});
565 this.owner.fireEvent('initialize', this);
568 // this is to prevent a href clicks resulting in a redirect?
570 onPasteEvent : function(e,v)
572 // I think we better assume paste is going to be a dirty load of rubish from word..
574 // even pasting into a 'email version' of this widget will have to clean up that mess.
575 var cd = (e.browserEvent.clipboardData || window.clipboardData);
577 // check what type of paste - if it's an image, then handle it differently.
578 if (cd.files && cd.files.length > 0) {
580 var urlAPI = (window.createObjectURL && window) ||
581 (window.URL && URL.revokeObjectURL && URL) ||
582 (window.webkitURL && webkitURL);
584 var url = urlAPI.createObjectURL( cd.files[0]);
585 this.insertAtCursor('<img src=" + url + ">');
588 if (cd.types.indexOf('text/html') < 0 ) {
592 var html = cd.getData('text/html'); // clipboard event
593 if (cd.types.indexOf('text/rtf') > -1) {
594 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
595 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
600 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
601 .map(function(g) { return g.toDataURL(); })
602 .filter(function(g) { return g != 'about:blank'; });
605 html = this.cleanWordChars(html);
607 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
610 var sn = this.getParentElement();
611 // check if d contains a table, and prevent nesting??
612 //Roo.log(d.getElementsByTagName('table'));
614 //Roo.log(sn.closest('table'));
615 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
617 this.insertAtCursor("You can not nest tables");
618 //Roo.log("prevent?"); // fixme -
624 if (images.length > 0) {
625 // replace all v:imagedata - with img.
626 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
627 Roo.each(ar, function(node) {
628 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
629 node.parentNode.removeChild(node);
633 Roo.each(d.getElementsByTagName('img'), function(img, i) {
634 img.setAttribute('src', images[i]);
637 if (this.autoClean) {
638 new Roo.htmleditor.FilterWord({ node : d });
640 new Roo.htmleditor.FilterStyleToTag({ node : d });
641 new Roo.htmleditor.FilterAttributes({
643 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
644 attrib_clean : ['href', 'src' ]
646 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
648 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
649 new Roo.htmleditor.FilterParagraph({ node : d });
650 new Roo.htmleditor.FilterSpan({ node : d });
651 new Roo.htmleditor.FilterLongBr({ node : d });
652 new Roo.htmleditor.FilterComment({ node : d });
656 if (this.enableBlocks) {
658 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
659 if (img.closest('figure')) { // assume!! that it's aready
662 var fig = new Roo.htmleditor.BlockFigure({
665 fig.updateElement(img); // replace it..
671 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
672 if (this.enableBlocks) {
673 Roo.htmleditor.Block.initAll(this.doc.body);
679 // default behaveiour should be our local cleanup paste? (optional?)
680 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
681 //this.owner.fireEvent('paste', e, v);
684 onDestroy : function(){
690 //for (var i =0; i < this.toolbars.length;i++) {
691 // // fixme - ask toolbars for heights?
692 // this.toolbars[i].onDestroy();
695 //this.wrap.dom.innerHTML = '';
696 //this.wrap.remove();
701 onFirstFocus : function(){
704 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
706 this.activated = true;
709 if(Roo.isGecko){ // prevent silly gecko errors
711 var s = this.win.getSelection();
712 if(!s.focusNode || s.focusNode.nodeType != 3){
713 var r = s.getRangeAt(0);
714 r.selectNodeContents((this.doc.body || this.doc.documentElement));
719 this.execCmd('useCSS', true);
720 this.execCmd('styleWithCSS', false);
723 this.owner.fireEvent('activate', this);
727 adjustFont: function(btn){
728 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
729 //if(Roo.isSafari){ // safari
732 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
733 if(Roo.isSafari){ // safari
734 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
735 v = (v < 10) ? 10 : v;
736 v = (v > 48) ? 48 : v;
737 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
742 v = Math.max(1, v+adjust);
744 this.execCmd('FontSize', v );
747 onEditorEvent : function(e)
751 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
752 return; // we do not handle this.. (undo manager does..)
754 // in theory this detects if the last element is not a br, then we try and do that.
755 // its so clicking in space at bottom triggers adding a br and moving the cursor.
757 e.target.nodeName == 'BODY' &&
758 e.type == "mouseup" &&
759 this.doc.body.lastChild
761 var lc = this.doc.body.lastChild;
762 // gtx-trans is google translate plugin adding crap.
763 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
764 lc = lc.previousSibling;
766 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
767 // if last element is <BR> - then dont do anything.
769 var ns = this.doc.createElement('br');
770 this.doc.body.appendChild(ns);
771 range = this.doc.createRange();
772 range.setStartAfter(ns);
773 range.collapse(true);
774 var sel = this.win.getSelection();
775 sel.removeAllRanges();
782 this.fireEditorEvent(e);
783 // this.updateToolbar();
784 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
787 fireEditorEvent: function(e)
789 this.owner.fireEvent('editorevent', this, e);
792 insertTag : function(tg)
794 // could be a bit smarter... -> wrap the current selected tRoo..
795 if (tg.toLowerCase() == 'span' ||
796 tg.toLowerCase() == 'code' ||
797 tg.toLowerCase() == 'sup' ||
798 tg.toLowerCase() == 'sub'
801 range = this.createRange(this.getSelection());
802 var wrappingNode = this.doc.createElement(tg.toLowerCase());
803 wrappingNode.appendChild(range.extractContents());
804 range.insertNode(wrappingNode);
811 this.execCmd("formatblock", tg);
812 this.undoManager.addEvent();
815 insertText : function(txt)
819 var range = this.createRange();
820 range.deleteContents();
821 //alert(Sender.getAttribute('label'));
823 range.insertNode(this.doc.createTextNode(txt));
824 this.undoManager.addEvent();
830 * Executes a Midas editor command on the editor document and performs necessary focus and
831 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
832 * @param {String} cmd The Midas command
833 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
835 relayCmd : function(cmd, value)
841 case 'justifycenter':
842 // if we are in a cell, then we will adjust the
843 var n = this.getParentElement();
844 var td = n.closest('td');
846 var bl = Roo.htmleditor.Block.factory(td);
847 bl.textAlign = cmd.replace('justify','');
849 this.owner.fireEvent('editorevent', this);
852 this.execCmd('styleWithCSS', true); //
856 // if there is no selection, then we insert, and set the curson inside it..
857 this.execCmd('styleWithCSS', false);
867 this.execCmd(cmd, value);
868 this.owner.fireEvent('editorevent', this);
869 //this.updateToolbar();
870 this.owner.deferFocus();
874 * Executes a Midas editor command directly on the editor document.
875 * For visual commands, you should use {@link #relayCmd} instead.
876 * <b>This should only be called after the editor is initialized.</b>
877 * @param {String} cmd The Midas command
878 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
880 execCmd : function(cmd, value){
881 this.doc.execCommand(cmd, false, value === undefined ? null : value);
888 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
890 * @param {String} text | dom node..
892 insertAtCursor : function(text)
899 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
903 // from jquery ui (MIT licenced)
907 if (win.getSelection && win.getSelection().getRangeAt) {
909 // delete the existing?
911 this.createRange(this.getSelection()).deleteContents();
912 range = win.getSelection().getRangeAt(0);
913 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
914 range.insertNode(node);
915 range = range.cloneRange();
916 range.collapse(false);
918 win.getSelection().removeAllRanges();
919 win.getSelection().addRange(range);
923 } else if (win.document.selection && win.document.selection.createRange) {
924 // no firefox support
925 var txt = typeof(text) == 'string' ? text : text.outerHTML;
926 win.document.selection.createRange().pasteHTML(txt);
929 // no firefox support
930 var txt = typeof(text) == 'string' ? text : text.outerHTML;
931 this.execCmd('InsertHTML', txt);
939 mozKeyPress : function(e){
941 var c = e.getCharCode(), cmd;
944 c = String.fromCharCode(c).toLowerCase();
958 // this.cleanUpPaste.defer(100, this);
976 fixKeys : function(){ // load time branching for fastest keydown performance
981 var k = e.getKey(), r;
984 r = this.doc.selection.createRange();
987 r.pasteHTML('    ');
992 /// this is handled by Roo.htmleditor.KeyEnter
995 r = this.doc.selection.createRange();
997 var target = r.parentElement();
998 if(!target || target.tagName.toLowerCase() != 'li'){
1000 r.pasteHTML('<br/>');
1007 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1008 // this.cleanUpPaste.defer(100, this);
1014 }else if(Roo.isOpera){
1020 this.execCmd('InsertHTML','    ');
1024 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1025 // this.cleanUpPaste.defer(100, this);
1030 }else if(Roo.isSafari){
1036 this.execCmd('InsertText','\t');
1040 this.mozKeyPress(e);
1042 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1043 // this.cleanUpPaste.defer(100, this);
1051 getAllAncestors: function()
1053 var p = this.getSelectedNode();
1056 a.push(p); // push blank onto stack..
1057 p = this.getParentElement();
1061 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1065 a.push(this.doc.body);
1069 lastSelNode : false,
1072 getSelection : function()
1074 this.assignDocWin();
1075 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1079 * @param {DomElement} node the node to select
1081 selectNode : function(node, collapse)
1083 var nodeRange = node.ownerDocument.createRange();
1085 nodeRange.selectNode(node);
1087 nodeRange.selectNodeContents(node);
1089 if (collapse === true) {
1090 nodeRange.collapse(true);
1093 var s = this.win.getSelection();
1094 s.removeAllRanges();
1095 s.addRange(nodeRange);
1098 getSelectedNode: function()
1100 // this may only work on Gecko!!!
1102 // should we cache this!!!!
1106 var range = this.createRange(this.getSelection()).cloneRange();
1109 var parent = range.parentElement();
1111 var testRange = range.duplicate();
1112 testRange.moveToElementText(parent);
1113 if (testRange.inRange(range)) {
1116 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1119 parent = parent.parentElement;
1124 // is ancestor a text element.
1125 var ac = range.commonAncestorContainer;
1126 if (ac.nodeType == 3) {
1130 var ar = ac.childNodes;
1133 var other_nodes = [];
1134 var has_other_nodes = false;
1135 for (var i=0;i<ar.length;i++) {
1136 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1139 // fullly contained node.
1141 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1146 // probably selected..
1147 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1148 other_nodes.push(ar[i]);
1152 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1157 has_other_nodes = true;
1159 if (!nodes.length && other_nodes.length) {
1162 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1170 createRange: function(sel)
1172 // this has strange effects when using with
1173 // top toolbar - not sure if it's a great idea.
1174 //this.editor.contentWindow.focus();
1175 if (typeof sel != "undefined") {
1177 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1179 return this.doc.createRange();
1182 return this.doc.createRange();
1185 getParentElement: function()
1188 this.assignDocWin();
1189 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1191 var range = this.createRange(sel);
1194 var p = range.commonAncestorContainer;
1195 while (p.nodeType == 3) { // text node
1206 * Range intersection.. the hard stuff...
1210 * [ -- selected range --- ]
1214 * if end is before start or hits it. fail.
1215 * if start is after end or hits it fail.
1217 * if either hits (but other is outside. - then it's not
1223 // @see http://www.thismuchiknow.co.uk/?p=64.
1224 rangeIntersectsNode : function(range, node)
1226 var nodeRange = node.ownerDocument.createRange();
1228 nodeRange.selectNode(node);
1230 nodeRange.selectNodeContents(node);
1233 var rangeStartRange = range.cloneRange();
1234 rangeStartRange.collapse(true);
1236 var rangeEndRange = range.cloneRange();
1237 rangeEndRange.collapse(false);
1239 var nodeStartRange = nodeRange.cloneRange();
1240 nodeStartRange.collapse(true);
1242 var nodeEndRange = nodeRange.cloneRange();
1243 nodeEndRange.collapse(false);
1245 return rangeStartRange.compareBoundaryPoints(
1246 Range.START_TO_START, nodeEndRange) == -1 &&
1247 rangeEndRange.compareBoundaryPoints(
1248 Range.START_TO_START, nodeStartRange) == 1;
1252 rangeCompareNode : function(range, node)
1254 var nodeRange = node.ownerDocument.createRange();
1256 nodeRange.selectNode(node);
1258 nodeRange.selectNodeContents(node);
1262 range.collapse(true);
1264 nodeRange.collapse(true);
1266 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1267 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1269 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1271 var nodeIsBefore = ss == 1;
1272 var nodeIsAfter = ee == -1;
1274 if (nodeIsBefore && nodeIsAfter) {
1277 if (!nodeIsBefore && nodeIsAfter) {
1278 return 1; //right trailed.
1281 if (nodeIsBefore && !nodeIsAfter) {
1282 return 2; // left trailed.
1288 cleanWordChars : function(input) {// change the chars to hex code
1291 [ 8211, "–" ],
1292 [ 8212, "—" ],
1301 Roo.each(swapCodes, function(sw) {
1302 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1304 output = output.replace(swapper, sw[1]);
1314 cleanUpChild : function (node)
1317 new Roo.htmleditor.FilterComment({node : node});
1318 new Roo.htmleditor.FilterAttributes({
1320 attrib_black : this.ablack,
1321 attrib_clean : this.aclean,
1322 style_white : this.cwhite,
1323 style_black : this.cblack
1325 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1326 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1332 * Clean up MS wordisms...
1333 * @deprecated - use filter directly
1335 cleanWord : function(node)
1337 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1338 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1345 * @deprecated - use filters
1347 cleanTableWidths : function(node)
1349 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1356 applyBlacklists : function()
1358 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1359 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1361 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1362 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1363 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1367 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1368 if (b.indexOf(tag) > -1) {
1371 this.white.push(tag);
1375 Roo.each(w, function(tag) {
1376 if (b.indexOf(tag) > -1) {
1379 if (this.white.indexOf(tag) > -1) {
1382 this.white.push(tag);
1387 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1388 if (w.indexOf(tag) > -1) {
1391 this.black.push(tag);
1395 Roo.each(b, function(tag) {
1396 if (w.indexOf(tag) > -1) {
1399 if (this.black.indexOf(tag) > -1) {
1402 this.black.push(tag);
1407 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1408 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1412 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1413 if (b.indexOf(tag) > -1) {
1416 this.cwhite.push(tag);
1420 Roo.each(w, function(tag) {
1421 if (b.indexOf(tag) > -1) {
1424 if (this.cwhite.indexOf(tag) > -1) {
1427 this.cwhite.push(tag);
1432 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1433 if (w.indexOf(tag) > -1) {
1436 this.cblack.push(tag);
1440 Roo.each(b, function(tag) {
1441 if (w.indexOf(tag) > -1) {
1444 if (this.cblack.indexOf(tag) > -1) {
1447 this.cblack.push(tag);
1452 setStylesheets : function(stylesheets)
1454 if(typeof(stylesheets) == 'string'){
1455 Roo.get(this.iframe.contentDocument.head).createChild({
1466 Roo.each(stylesheets, function(s) {
1471 Roo.get(_this.iframe.contentDocument.head).createChild({
1483 updateLanguage : function()
1485 if (!this.iframe || !this.iframe.contentDocument) {
1488 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1492 removeStylesheets : function()
1496 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1501 setStyle : function(style)
1503 Roo.get(this.iframe.contentDocument.head).createChild({
1512 // hide stuff that is not compatible
1530 * @cfg {String} fieldClass @hide
1533 * @cfg {String} focusClass @hide
1536 * @cfg {String} autoCreate @hide
1539 * @cfg {String} inputType @hide
1542 * @cfg {String} invalidClass @hide
1545 * @cfg {String} invalidText @hide
1548 * @cfg {String} msgFx @hide
1551 * @cfg {String} validateOnBlur @hide
1555 Roo.HtmlEditorCore.white = [
1556 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1558 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1559 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1560 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1561 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1562 'TABLE', 'UL', 'XMP',
1564 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1567 'DIR', 'MENU', 'OL', 'UL', 'DL',
1573 Roo.HtmlEditorCore.black = [
1574 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1576 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1577 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1578 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1579 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1580 //'FONT' // CLEAN LATER..
1581 'COLGROUP', 'COL' // messy tables.
1585 Roo.HtmlEditorCore.clean = [ // ?? needed???
1586 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1588 Roo.HtmlEditorCore.tag_remove = [
1593 Roo.HtmlEditorCore.ablack = [
1597 Roo.HtmlEditorCore.aclean = [
1598 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1602 Roo.HtmlEditorCore.pwhite= [
1603 'http', 'https', 'mailto'
1606 // white listed style attributes.
1607 Roo.HtmlEditorCore.cwhite= [
1608 // 'text-align', /// default is to allow most things..
1614 // black listed style attributes.
1615 Roo.HtmlEditorCore.cblack= [
1616 // 'font-size' -- this can be set by the project