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({
407 attrib_clean : ['href', 'src' ]
410 var tidy = new Roo.htmleditor.TidySerializer({
413 html = tidy.serialize(div);
419 var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element!
420 var m = bs ? bs.match(/text-align:(.*?);/i) : false;
422 html = '<div style="'+m[0]+'">' + html + '</div>';
425 html = this.cleanHtml(html);
426 // fix up the special chars.. normaly like back quotes in word...
427 // however we do not want to do this with chinese..
428 html = html.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u0080-\uFFFF]/g, function(match) {
430 var cc = match.charCodeAt();
432 // Get the character value, handling surrogate pairs
433 if (match.length == 2) {
434 // It's a surrogate pair, calculate the Unicode code point
435 var high = match.charCodeAt(0) - 0xD800;
436 var low = match.charCodeAt(1) - 0xDC00;
437 cc = (high * 0x400) + low + 0x10000;
439 (cc >= 0x4E00 && cc < 0xA000 ) ||
440 (cc >= 0x3400 && cc < 0x4E00 ) ||
441 (cc >= 0xf900 && cc < 0xfb00 )
446 // No, use a numeric entity. Here we brazenly (and possibly mistakenly)
447 return "&#" + cc + ";";
454 if(this.owner.fireEvent('beforesync', this, html) !== false){
455 this.el.dom.value = html;
456 this.owner.fireEvent('sync', this, html);
462 * TEXTAREA -> EDITABLE
463 * Protected method that will not generally be called directly. Pushes the value of the textarea
464 * into the iframe editor.
466 pushValue : function()
468 //Roo.log("HtmlEditorCore:pushValue (TEXT->EDITOR)");
469 if(this.initialized){
470 var v = this.el.dom.value.trim();
473 if(this.owner.fireEvent('beforepush', this, v) !== false){
474 var d = (this.doc.body || this.doc.documentElement);
477 this.el.dom.value = d.innerHTML;
478 this.owner.fireEvent('push', this, v);
480 if (this.autoClean) {
481 new Roo.htmleditor.FilterParagraph({node : this.doc.body}); // paragraphs
482 new Roo.htmleditor.FilterSpan({node : this.doc.body}); // empty spans
484 if (this.enableBlocks) {
485 Roo.htmleditor.Block.initAll(this.doc.body);
488 this.updateLanguage();
490 var lc = this.doc.body.lastChild;
491 if (lc && lc.nodeType == 1 && lc.getAttribute("contenteditable") == "false") {
492 // add an extra line at the end.
493 this.doc.body.appendChild(this.doc.createElement('br'));
501 deferFocus : function(){
502 this.focus.defer(10, this);
507 if(this.win && !this.sourceEditMode){
514 assignDocWin: function()
516 var iframe = this.iframe;
519 this.doc = iframe.contentWindow.document;
520 this.win = iframe.contentWindow;
522 // if (!Roo.get(this.frameId)) {
525 // this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
526 // this.win = Roo.get(this.frameId).dom.contentWindow;
528 if (!Roo.get(this.frameId) && !iframe.contentDocument) {
532 this.doc = (iframe.contentDocument || Roo.get(this.frameId).dom.document);
533 this.win = (iframe.contentWindow || Roo.get(this.frameId).dom.contentWindow);
538 initEditor : function(){
539 //console.log("INIT EDITOR");
544 this.doc.designMode="on";
546 this.doc.write(this.getDocMarkup());
549 var dbody = (this.doc.body || this.doc.documentElement);
550 //var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat');
551 // this copies styles from the containing element into thsi one..
552 // not sure why we need all of this..
553 //var ss = this.el.getStyles('font-size', 'background-image', 'background-repeat');
555 //var ss = this.el.getStyles( 'background-image', 'background-repeat');
556 //ss['background-attachment'] = 'fixed'; // w3c
557 dbody.bgProperties = 'fixed'; // ie
558 dbody.setAttribute("translate", "no");
560 //Roo.DomHelper.applyStyles(dbody, ss);
561 Roo.EventManager.on(this.doc, {
563 'mouseup': this.onEditorEvent,
564 'dblclick': this.onEditorEvent,
565 'click': this.onEditorEvent,
566 'keyup': this.onEditorEvent,
571 Roo.EventManager.on(this.doc, {
572 'paste': this.onPasteEvent,
576 Roo.EventManager.on(this.doc, 'keypress', this.mozKeyPress, this);
579 if(Roo.isIE || Roo.isSafari || Roo.isOpera){
580 Roo.EventManager.on(this.doc, 'keydown', this.fixKeys, this);
582 this.initialized = true;
585 // initialize special key events - enter
586 new Roo.htmleditor.KeyEnter({core : this});
590 this.owner.fireEvent('initialize', this);
593 // this is to prevent a href clicks resulting in a redirect?
595 onPasteEvent : function(e,v)
597 // I think we better assume paste is going to be a dirty load of rubish from word..
599 // even pasting into a 'email version' of this widget will have to clean up that mess.
600 var cd = (e.browserEvent.clipboardData || window.clipboardData);
602 // check what type of paste - if it's an image, then handle it differently.
603 if (cd.files && cd.files.length > 0) {
605 var urlAPI = (window.createObjectURL && window) ||
606 (window.URL && URL.revokeObjectURL && URL) ||
607 (window.webkitURL && webkitURL);
609 var url = urlAPI.createObjectURL( cd.files[0]);
610 this.insertAtCursor('<img src=" + url + ">');
613 if (cd.types.indexOf('text/html') < 0 ) {
617 var html = cd.getData('text/html'); // clipboard event
618 if (cd.types.indexOf('text/rtf') > -1) {
619 var parser = new Roo.rtf.Parser(cd.getData('text/rtf'));
620 images = parser.doc ? parser.doc.getElementsByType('pict') : [];
625 images = images.filter(function(g) { return !g.path.match(/^rtf\/(head|pgdsctbl|listtable|footerf)/); }) // ignore headers/footers etc.
626 .map(function(g) { return g.toDataURL(); })
627 .filter(function(g) { return g != 'about:blank'; });
630 html = this.cleanWordChars(html);
632 var d = (new DOMParser().parseFromString(html, 'text/html')).body;
635 var sn = this.getParentElement();
636 // check if d contains a table, and prevent nesting??
637 //Roo.log(d.getElementsByTagName('table'));
639 //Roo.log(sn.closest('table'));
640 if (d.getElementsByTagName('table').length && sn && sn.closest('table')) {
642 this.insertAtCursor("You can not nest tables");
643 //Roo.log("prevent?"); // fixme -
649 if (images.length > 0) {
650 // replace all v:imagedata - with img.
651 var ar = Array.from(d.getElementsByTagName('v:imagedata'));
652 Roo.each(ar, function(node) {
653 node.parentNode.insertBefore(d.ownerDocument.createElement('img'), node );
654 node.parentNode.removeChild(node);
658 Roo.each(d.getElementsByTagName('img'), function(img, i) {
659 img.setAttribute('src', images[i]);
662 if (this.autoClean) {
663 new Roo.htmleditor.FilterWord({ node : d });
665 new Roo.htmleditor.FilterStyleToTag({ node : d });
666 new Roo.htmleditor.FilterAttributes({
668 attrib_white : ['href', 'src', 'name', 'align', 'colspan', 'rowspan', 'data-display', 'data-width', 'start'],
669 attrib_clean : ['href', 'src' ]
671 new Roo.htmleditor.FilterBlack({ node : d, tag : this.black});
673 new Roo.htmleditor.FilterKeepChildren({node : d, tag : [ 'FONT', ':' ]} );
674 new Roo.htmleditor.FilterParagraph({ node : d });
675 new Roo.htmleditor.FilterSpan({ node : d });
676 new Roo.htmleditor.FilterLongBr({ node : d });
677 new Roo.htmleditor.FilterComment({ node : d });
681 if (this.enableBlocks) {
683 Array.from(d.getElementsByTagName('img')).forEach(function(img) {
684 if (img.closest('figure')) { // assume!! that it's aready
687 var fig = new Roo.htmleditor.BlockFigure({
690 fig.updateElement(img); // replace it..
696 this.insertAtCursor(d.innerHTML.replace(/ /g,' '));
697 if (this.enableBlocks) {
698 Roo.htmleditor.Block.initAll(this.doc.body);
703 Roo.log("HTML EDITOR ON PASTEEE");
704 this.owner.fireEvent('paste', this);
706 // default behaveiour should be our local cleanup paste? (optional?)
707 // for simple editor - we want to hammer the paste and get rid of everything... - so over-rideable..
708 //this.owner.fireEvent('paste', e, v);
711 onDestroy : function(){
717 //for (var i =0; i < this.toolbars.length;i++) {
718 // // fixme - ask toolbars for heights?
719 // this.toolbars[i].onDestroy();
722 //this.wrap.dom.innerHTML = '';
723 //this.wrap.remove();
728 onFirstFocus : function(){
731 this.undoManager = new Roo.lib.UndoManager(100,(this.doc.body || this.doc.documentElement));
733 this.activated = true;
736 if(Roo.isGecko){ // prevent silly gecko errors
738 var s = this.win.getSelection();
739 if(!s.focusNode || s.focusNode.nodeType != 3){
740 var r = s.getRangeAt(0);
741 r.selectNodeContents((this.doc.body || this.doc.documentElement));
746 this.execCmd('useCSS', true);
747 this.execCmd('styleWithCSS', false);
750 this.owner.fireEvent('activate', this);
754 adjustFont: function(btn){
755 var adjust = btn.cmd == 'increasefontsize' ? 1 : -1;
756 //if(Roo.isSafari){ // safari
759 var v = parseInt(this.doc.queryCommandValue('FontSize')|| 3, 10);
760 if(Roo.isSafari){ // safari
761 var sm = { 10 : 1, 13: 2, 16:3, 18:4, 24: 5, 32:6, 48: 7 };
762 v = (v < 10) ? 10 : v;
763 v = (v > 48) ? 48 : v;
764 v = typeof(sm[v]) == 'undefined' ? 1 : sm[v];
769 v = Math.max(1, v+adjust);
771 this.execCmd('FontSize', v );
774 onEditorEvent : function(e)
778 if (e && (e.ctrlKey || e.metaKey) && e.keyCode === 90) {
779 return; // we do not handle this.. (undo manager does..)
781 // in theory this detects if the last element is not a br, then we try and do that.
782 // its so clicking in space at bottom triggers adding a br and moving the cursor.
784 e.target.nodeName == 'BODY' &&
785 e.type == "mouseup" &&
786 this.doc.body.lastChild
788 var lc = this.doc.body.lastChild;
789 // gtx-trans is google translate plugin adding crap.
790 while ((lc.nodeType == 3 && lc.nodeValue == '') || lc.id == 'gtx-trans') {
791 lc = lc.previousSibling;
793 if (lc.nodeType == 1 && lc.nodeName != 'BR') {
794 // if last element is <BR> - then dont do anything.
796 var ns = this.doc.createElement('br');
797 this.doc.body.appendChild(ns);
798 range = this.doc.createRange();
799 range.setStartAfter(ns);
800 range.collapse(true);
801 var sel = this.win.getSelection();
802 sel.removeAllRanges();
809 this.fireEditorEvent(e);
810 // this.updateToolbar();
811 this.syncValue(); //we can not sync so often.. sync cleans, so this breaks stuff
814 fireEditorEvent: function(e)
816 this.owner.fireEvent('editorevent', this, e);
819 insertTag : function(tg)
821 // could be a bit smarter... -> wrap the current selected tRoo..
822 if (tg.toLowerCase() == 'span' ||
823 tg.toLowerCase() == 'code' ||
824 tg.toLowerCase() == 'sup' ||
825 tg.toLowerCase() == 'sub'
828 range = this.createRange(this.getSelection());
829 var wrappingNode = this.doc.createElement(tg.toLowerCase());
830 wrappingNode.appendChild(range.extractContents());
831 range.insertNode(wrappingNode);
838 this.execCmd("formatblock", tg);
839 this.undoManager.addEvent();
842 insertText : function(txt)
846 var range = this.createRange();
847 range.deleteContents();
848 //alert(Sender.getAttribute('label'));
850 range.insertNode(this.doc.createTextNode(txt));
851 this.undoManager.addEvent();
857 * Executes a Midas editor command on the editor document and performs necessary focus and
858 * toolbar updates. <b>This should only be called after the editor is initialized.</b>
859 * @param {String} cmd The Midas command
860 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
862 relayCmd : function(cmd, value)
868 case 'justifycenter':
869 // if we are in a cell, then we will adjust the
870 var n = this.getParentElement();
871 var td = n.closest('td');
873 var bl = Roo.htmleditor.Block.factory(td);
874 bl.textAlign = cmd.replace('justify','');
876 this.owner.fireEvent('editorevent', this);
879 this.execCmd('styleWithCSS', true); //
883 // if there is no selection, then we insert, and set the curson inside it..
884 this.execCmd('styleWithCSS', false);
894 this.execCmd(cmd, value);
895 this.owner.fireEvent('editorevent', this);
896 //this.updateToolbar();
897 this.owner.deferFocus();
901 * Executes a Midas editor command directly on the editor document.
902 * For visual commands, you should use {@link #relayCmd} instead.
903 * <b>This should only be called after the editor is initialized.</b>
904 * @param {String} cmd The Midas command
905 * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null)
907 execCmd : function(cmd, value){
908 this.doc.execCommand(cmd, false, value === undefined ? null : value);
915 * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated
917 * @param {String} text | dom node..
919 insertAtCursor : function(text)
926 if(Roo.isGecko || Roo.isOpera || Roo.isSafari){
930 // from jquery ui (MIT licenced)
934 if (win.getSelection && win.getSelection().getRangeAt) {
936 // delete the existing?
938 this.createRange(this.getSelection()).deleteContents();
939 range = win.getSelection().getRangeAt(0);
940 node = typeof(text) == 'string' ? range.createContextualFragment(text) : text;
941 range.insertNode(node);
942 range = range.cloneRange();
943 range.collapse(false);
945 win.getSelection().removeAllRanges();
946 win.getSelection().addRange(range);
950 } else if (win.document.selection && win.document.selection.createRange) {
951 // no firefox support
952 var txt = typeof(text) == 'string' ? text : text.outerHTML;
953 win.document.selection.createRange().pasteHTML(txt);
956 // no firefox support
957 var txt = typeof(text) == 'string' ? text : text.outerHTML;
958 this.execCmd('InsertHTML', txt);
966 mozKeyPress : function(e){
968 var c = e.getCharCode(), cmd;
971 c = String.fromCharCode(c).toLowerCase();
985 // this.cleanUpPaste.defer(100, this);
1003 fixKeys : function(){ // load time branching for fastest keydown performance
1008 var k = e.getKey(), r;
1011 r = this.doc.selection.createRange();
1014 r.pasteHTML('    ');
1019 /// this is handled by Roo.htmleditor.KeyEnter
1022 r = this.doc.selection.createRange();
1024 var target = r.parentElement();
1025 if(!target || target.tagName.toLowerCase() != 'li'){
1027 r.pasteHTML('<br/>');
1034 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1035 // this.cleanUpPaste.defer(100, this);
1041 }else if(Roo.isOpera){
1047 this.execCmd('InsertHTML','    ');
1051 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1052 // this.cleanUpPaste.defer(100, this);
1057 }else if(Roo.isSafari){
1063 this.execCmd('InsertText','\t');
1067 this.mozKeyPress(e);
1069 //if (String.fromCharCode(k).toLowerCase() == 'v') { // paste
1070 // this.cleanUpPaste.defer(100, this);
1078 getAllAncestors: function()
1080 var p = this.getSelectedNode();
1083 a.push(p); // push blank onto stack..
1084 p = this.getParentElement();
1088 while (p && (p.nodeType == 1) && (p.tagName.toLowerCase() != 'body')) {
1092 a.push(this.doc.body);
1096 lastSelNode : false,
1099 getSelection : function()
1101 this.assignDocWin();
1102 return Roo.lib.Selection.wrap(Roo.isIE ? this.doc.selection : this.win.getSelection(), this.doc);
1106 * @param {DomElement} node the node to select
1108 selectNode : function(node, collapse)
1110 var nodeRange = node.ownerDocument.createRange();
1112 nodeRange.selectNode(node);
1114 nodeRange.selectNodeContents(node);
1116 if (collapse === true) {
1117 nodeRange.collapse(true);
1120 var s = this.win.getSelection();
1121 s.removeAllRanges();
1122 s.addRange(nodeRange);
1125 getSelectedNode: function()
1127 // this may only work on Gecko!!!
1129 // should we cache this!!!!
1133 var range = this.createRange(this.getSelection()).cloneRange();
1136 var parent = range.parentElement();
1138 var testRange = range.duplicate();
1139 testRange.moveToElementText(parent);
1140 if (testRange.inRange(range)) {
1143 if ((parent.nodeType != 1) || (parent.tagName.toLowerCase() == 'body')) {
1146 parent = parent.parentElement;
1151 // is ancestor a text element.
1152 var ac = range.commonAncestorContainer;
1153 if (ac.nodeType == 3) {
1157 var ar = ac.childNodes;
1160 var other_nodes = [];
1161 var has_other_nodes = false;
1162 for (var i=0;i<ar.length;i++) {
1163 if ((ar[i].nodeType == 3) && (!ar[i].data.length)) { // empty text ?
1166 // fullly contained node.
1168 if (this.rangeIntersectsNode(range,ar[i]) && this.rangeCompareNode(range,ar[i]) == 3) {
1173 // probably selected..
1174 if ((ar[i].nodeType == 1) && this.rangeIntersectsNode(range,ar[i]) && (this.rangeCompareNode(range,ar[i]) > 0)) {
1175 other_nodes.push(ar[i]);
1179 if (!this.rangeIntersectsNode(range,ar[i])|| (this.rangeCompareNode(range,ar[i]) == 0)) {
1184 has_other_nodes = true;
1186 if (!nodes.length && other_nodes.length) {
1189 if (has_other_nodes || !nodes.length || (nodes.length > 1)) {
1197 createRange: function(sel)
1199 // this has strange effects when using with
1200 // top toolbar - not sure if it's a great idea.
1201 //this.editor.contentWindow.focus();
1202 if (typeof sel != "undefined") {
1204 return sel.getRangeAt ? sel.getRangeAt(0) : sel.createRange();
1206 return this.doc.createRange();
1209 return this.doc.createRange();
1212 getParentElement: function()
1215 this.assignDocWin();
1216 var sel = Roo.isIE ? this.doc.selection : this.win.getSelection();
1218 var range = this.createRange(sel);
1221 var p = range.commonAncestorContainer;
1222 while (p.nodeType == 3) { // text node
1233 * Range intersection.. the hard stuff...
1237 * [ -- selected range --- ]
1241 * if end is before start or hits it. fail.
1242 * if start is after end or hits it fail.
1244 * if either hits (but other is outside. - then it's not
1250 // @see http://www.thismuchiknow.co.uk/?p=64.
1251 rangeIntersectsNode : function(range, node)
1253 var nodeRange = node.ownerDocument.createRange();
1255 nodeRange.selectNode(node);
1257 nodeRange.selectNodeContents(node);
1260 var rangeStartRange = range.cloneRange();
1261 rangeStartRange.collapse(true);
1263 var rangeEndRange = range.cloneRange();
1264 rangeEndRange.collapse(false);
1266 var nodeStartRange = nodeRange.cloneRange();
1267 nodeStartRange.collapse(true);
1269 var nodeEndRange = nodeRange.cloneRange();
1270 nodeEndRange.collapse(false);
1272 return rangeStartRange.compareBoundaryPoints(
1273 Range.START_TO_START, nodeEndRange) == -1 &&
1274 rangeEndRange.compareBoundaryPoints(
1275 Range.START_TO_START, nodeStartRange) == 1;
1279 rangeCompareNode : function(range, node)
1281 var nodeRange = node.ownerDocument.createRange();
1283 nodeRange.selectNode(node);
1285 nodeRange.selectNodeContents(node);
1289 range.collapse(true);
1291 nodeRange.collapse(true);
1293 var ss = range.compareBoundaryPoints( Range.START_TO_START, nodeRange);
1294 var ee = range.compareBoundaryPoints( Range.END_TO_END, nodeRange);
1296 //Roo.log(node.tagName + ': ss='+ss +', ee='+ee)
1298 var nodeIsBefore = ss == 1;
1299 var nodeIsAfter = ee == -1;
1301 if (nodeIsBefore && nodeIsAfter) {
1304 if (!nodeIsBefore && nodeIsAfter) {
1305 return 1; //right trailed.
1308 if (nodeIsBefore && !nodeIsAfter) {
1309 return 2; // left trailed.
1315 cleanWordChars : function(input) {// change the chars to hex code
1318 [ 8211, "–" ],
1319 [ 8212, "—" ],
1328 Roo.each(swapCodes, function(sw) {
1329 var swapper = new RegExp("\\u" + sw[0].toString(16), "g"); // hex codes
1331 output = output.replace(swapper, sw[1]);
1341 cleanUpChild : function (node)
1344 new Roo.htmleditor.FilterComment({node : node});
1345 new Roo.htmleditor.FilterAttributes({
1347 attrib_black : this.ablack,
1348 attrib_clean : this.aclean,
1349 style_white : this.cwhite,
1350 style_black : this.cblack
1352 new Roo.htmleditor.FilterBlack({ node : node, tag : this.black});
1353 new Roo.htmleditor.FilterKeepChildren({node : node, tag : this.tag_remove} );
1359 * Clean up MS wordisms...
1360 * @deprecated - use filter directly
1362 cleanWord : function(node)
1364 new Roo.htmleditor.FilterWord({ node : node ? node : this.doc.body });
1365 new Roo.htmleditor.FilterKeepChildren({node : node ? node : this.doc.body, tag : [ 'FONT', ':' ]} );
1372 * @deprecated - use filters
1374 cleanTableWidths : function(node)
1376 new Roo.htmleditor.FilterTableWidth({ node : node ? node : this.doc.body});
1383 applyBlacklists : function()
1385 var w = typeof(this.owner.white) != 'undefined' && this.owner.white ? this.owner.white : [];
1386 var b = typeof(this.owner.black) != 'undefined' && this.owner.black ? this.owner.black : [];
1388 this.aclean = typeof(this.owner.aclean) != 'undefined' && this.owner.aclean ? this.owner.aclean : Roo.HtmlEditorCore.aclean;
1389 this.ablack = typeof(this.owner.ablack) != 'undefined' && this.owner.ablack ? this.owner.ablack : Roo.HtmlEditorCore.ablack;
1390 this.tag_remove = typeof(this.owner.tag_remove) != 'undefined' && this.owner.tag_remove ? this.owner.tag_remove : Roo.HtmlEditorCore.tag_remove;
1394 Roo.each(Roo.HtmlEditorCore.white, function(tag) {
1395 if (b.indexOf(tag) > -1) {
1398 this.white.push(tag);
1402 Roo.each(w, function(tag) {
1403 if (b.indexOf(tag) > -1) {
1406 if (this.white.indexOf(tag) > -1) {
1409 this.white.push(tag);
1414 Roo.each(Roo.HtmlEditorCore.black, function(tag) {
1415 if (w.indexOf(tag) > -1) {
1418 this.black.push(tag);
1422 Roo.each(b, function(tag) {
1423 if (w.indexOf(tag) > -1) {
1426 if (this.black.indexOf(tag) > -1) {
1429 this.black.push(tag);
1434 w = typeof(this.owner.cwhite) != 'undefined' && this.owner.cwhite ? this.owner.cwhite : [];
1435 b = typeof(this.owner.cblack) != 'undefined' && this.owner.cblack ? this.owner.cblack : [];
1439 Roo.each(Roo.HtmlEditorCore.cwhite, function(tag) {
1440 if (b.indexOf(tag) > -1) {
1443 this.cwhite.push(tag);
1447 Roo.each(w, function(tag) {
1448 if (b.indexOf(tag) > -1) {
1451 if (this.cwhite.indexOf(tag) > -1) {
1454 this.cwhite.push(tag);
1459 Roo.each(Roo.HtmlEditorCore.cblack, function(tag) {
1460 if (w.indexOf(tag) > -1) {
1463 this.cblack.push(tag);
1467 Roo.each(b, function(tag) {
1468 if (w.indexOf(tag) > -1) {
1471 if (this.cblack.indexOf(tag) > -1) {
1474 this.cblack.push(tag);
1479 setStylesheets : function(stylesheets)
1481 if(typeof(stylesheets) == 'string'){
1482 Roo.get(this.iframe.contentDocument.head).createChild({
1493 Roo.each(stylesheets, function(s) {
1498 Roo.get(_this.iframe.contentDocument.head).createChild({
1510 updateLanguage : function()
1512 if (!this.iframe || !this.iframe.contentDocument) {
1515 Roo.get(this.iframe.contentDocument.body).attr("lang", this.language);
1519 removeStylesheets : function()
1523 Roo.each(Roo.get(_this.iframe.contentDocument.head).select('link[rel=stylesheet]', true).elements, function(s){
1528 setStyle : function(style)
1530 Roo.get(this.iframe.contentDocument.head).createChild({
1539 // hide stuff that is not compatible
1557 * @cfg {String} fieldClass @hide
1560 * @cfg {String} focusClass @hide
1563 * @cfg {String} autoCreate @hide
1566 * @cfg {String} inputType @hide
1569 * @cfg {String} invalidClass @hide
1572 * @cfg {String} invalidText @hide
1575 * @cfg {String} msgFx @hide
1578 * @cfg {String} validateOnBlur @hide
1582 Roo.HtmlEditorCore.white = [
1583 'AREA', 'BR', 'IMG', 'INPUT', 'HR', 'WBR',
1585 'ADDRESS', 'BLOCKQUOTE', 'CENTER', 'DD', 'DIR', 'DIV',
1586 'DL', 'DT', 'H1', 'H2', 'H3', 'H4',
1587 'H5', 'H6', 'HR', 'ISINDEX', 'LISTING', 'MARQUEE',
1588 'MENU', 'MULTICOL', 'OL', 'P', 'PLAINTEXT', 'PRE',
1589 'TABLE', 'UL', 'XMP',
1591 'CAPTION', 'COL', 'COLGROUP', 'TBODY', 'TD', 'TFOOT', 'TH',
1594 'DIR', 'MENU', 'OL', 'UL', 'DL',
1600 Roo.HtmlEditorCore.black = [
1601 // 'embed', 'object', // enable - backend responsiblity to clean thiese
1603 'BASE', 'BASEFONT', 'BGSOUND', 'BLINK', 'BODY',
1604 'FRAME', 'FRAMESET', 'HEAD', 'HTML', 'ILAYER',
1605 'IFRAME', 'LAYER', 'LINK', 'META', 'OBJECT',
1606 'SCRIPT', 'STYLE' ,'TITLE', 'XML',
1607 //'FONT' // CLEAN LATER..
1608 'COLGROUP', 'COL' // messy tables.
1612 Roo.HtmlEditorCore.clean = [ // ?? needed???
1613 'SCRIPT', 'STYLE', 'TITLE', 'XML'
1615 Roo.HtmlEditorCore.tag_remove = [
1620 Roo.HtmlEditorCore.ablack = [
1624 Roo.HtmlEditorCore.aclean = [
1625 'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc'
1629 Roo.HtmlEditorCore.pwhite= [
1630 'http', 'https', 'mailto'
1633 // white listed style attributes.
1634 Roo.HtmlEditorCore.cwhite= [
1635 // 'text-align', /// default is to allow most things..
1641 // black listed style attributes.
1642 Roo.HtmlEditorCore.cblack= [
1643 // 'font-size' -- this can be set by the project